diff --git a/.gitmodules b/.gitmodules index be6b9310a7874377e3871a65d28db01d1063973b..b64f1a6afca22fe370aedc912d735119c1b5bef1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,14 +22,6 @@ path = modules/EntityFrameworkCore url = https://github.com/aspnet/EntityFrameworkCore.git branch = release/2.1 -[submodule "modules/Hosting"] - path = modules/Hosting - url = https://github.com/aspnet/Hosting.git - branch = release/2.1 -[submodule "modules/HttpAbstractions"] - path = modules/HttpAbstractions - url = https://github.com/aspnet/HttpAbstractions.git - branch = release/2.1 [submodule "modules/HttpSysServer"] path = modules/HttpSysServer url = https://github.com/aspnet/HttpSysServer.git diff --git a/Directory.Build.props b/Directory.Build.props index 99d625f1ad5659390272422d7d35c3420d435737..1999dba76d08f30619e3a83f3faf5b2e5fe0c398 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -31,6 +31,8 @@ <IncludeSource>false</IncludeSource> <IncludeSymbols>true</IncludeSymbols> + <SharedSourceRoot>$(MSBuildThisFileDirectory)src\Shared\</SharedSourceRoot> + <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage> </PropertyGroup> diff --git a/build/artifacts.props b/build/artifacts.props index 717198331b425392276e55cc4d2e74b25768227f..587242f9e54db22e63c2744f66872aed1b4fe04e 100644 --- a/build/artifacts.props +++ b/build/artifacts.props @@ -74,7 +74,6 @@ <PackageArtifact Include="Microsoft.AspNetCore.HostFiltering" AllMetapackage="true" AppMetapackage="true" Category="ship" /> <PackageArtifact Include="Microsoft.AspNetCore.Hosting.Abstractions" AllMetapackage="true" AppMetapackage="true" Category="ship" /> <PackageArtifact Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" AllMetapackage="true" AppMetapackage="true" Category="ship" /> - <PackageArtifact Include="Microsoft.AspNetCore.Hosting.WebHostBuilderFactory.Sources" Category="noship" /> <PackageArtifact Include="Microsoft.AspNetCore.Hosting.WindowsServices" Category="ship" /> <PackageArtifact Include="Microsoft.AspNetCore.Hosting" AllMetapackage="true" AppMetapackage="true" Category="ship" /> <PackageArtifact Include="Microsoft.AspNetCore.Html.Abstractions" AllMetapackage="true" AppMetapackage="true" Category="ship" /> @@ -134,7 +133,6 @@ <PackageArtifact Include="Microsoft.AspNetCore.Routing" AllMetapackage="true" AppMetapackage="true" Category="ship" /> <PackageArtifact Include="Microsoft.AspNetCore.Server.HttpSys" AllMetapackage="true" AppMetapackage="true" Category="ship" /> <PackageArtifact Include="Microsoft.AspNetCore.Server.IISIntegration" AllMetapackage="true" AppMetapackage="true" Category="ship" /> - <PackageArtifact Include="Microsoft.AspNetCore.Server.IntegrationTesting" Category="noship" /> <PackageArtifact Include="Microsoft.AspNetCore.Server.Kestrel.Core" AllMetapackage="true" AppMetapackage="true" Category="ship" /> <PackageArtifact Include="Microsoft.AspNetCore.Server.Kestrel.Https" AllMetapackage="true" AppMetapackage="true" Category="ship" /> <PackageArtifact Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions" AllMetapackage="true" AppMetapackage="true" Category="ship" /> @@ -183,8 +181,6 @@ <PackageArtifact Include="Microsoft.Extensions.ApplicationModelDetection" Category="noship" /> <PackageArtifact Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Category="noship" /> <PackageArtifact Include="Microsoft.Extensions.Diagnostics.HealthChecks" Category="noship" /> - <PackageArtifact Include="Microsoft.Extensions.Hosting.Abstractions" AllMetapackage="true" AppMetapackage="true" Category="ship" /> - <PackageArtifact Include="Microsoft.Extensions.Hosting" AllMetapackage="true" AppMetapackage="true" Category="ship" /> <PackageArtifact Include="Microsoft.Extensions.Identity.Core" AllMetapackage="true" AppMetapackage="true" Category="ship" /> <PackageArtifact Include="Microsoft.Extensions.Identity.Stores" AllMetapackage="true" AppMetapackage="true" Category="ship" /> <PackageArtifact Include="Microsoft.Extensions.Localization.Abstractions" AllMetapackage="true" AppMetapackage="true" Category="ship" /> diff --git a/build/buildorder.props b/build/buildorder.props index d5b5dab276bad9ca26fb36598d9840dc73c4139b..58a3f1f7664e754931f6ba2dd379c12df91ad0a2 100644 --- a/build/buildorder.props +++ b/build/buildorder.props @@ -8,8 +8,6 @@ <ItemGroup> <RepositoryBuildOrder Include="Razor" Order="6" /> - <RepositoryBuildOrder Include="HttpAbstractions" Order="6" /> - <RepositoryBuildOrder Include="Hosting" Order="7" /> <RepositoryBuildOrder Include="EntityFrameworkCore" Order="8" /> <RepositoryBuildOrder Include="HttpSysServer" Order="8" /> <RepositoryBuildOrder Include="BrowserLink" Order="8" /> diff --git a/build/dependencies.props b/build/dependencies.props index b46e6e7a6c63454ec7727c51bbdb49ee17cc7d14..8b1e4119f1373527ce4021c048e2250719655a9c 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -44,13 +44,13 @@ <MicrosoftExtensionsConfigurationIniPackageVersion>2.1.1</MicrosoftExtensionsConfigurationIniPackageVersion> <MicrosoftExtensionsConfigurationJsonPackageVersion>2.1.1</MicrosoftExtensionsConfigurationJsonPackageVersion> <MicrosoftExtensionsConfigurationKeyPerFilePackageVersion>2.1.1</MicrosoftExtensionsConfigurationKeyPerFilePackageVersion> + <MicrosoftExtensionsConfigurationPackageVersion>2.1.1</MicrosoftExtensionsConfigurationPackageVersion> <MicrosoftExtensionsConfigurationUserSecretsPackageVersion>2.1.1</MicrosoftExtensionsConfigurationUserSecretsPackageVersion> <MicrosoftExtensionsConfigurationXmlPackageVersion>2.1.1</MicrosoftExtensionsConfigurationXmlPackageVersion> - <MicrosoftExtensionsConfigurationPackageVersion>2.1.1</MicrosoftExtensionsConfigurationPackageVersion> <MicrosoftExtensionsCopyOnWriteDictionarySourcesPackageVersion>2.1.1</MicrosoftExtensionsCopyOnWriteDictionarySourcesPackageVersion> <MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>2.1.1</MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion> - <MicrosoftExtensionsDependencyInjectionSpecificationTestsPackageVersion>2.1.1</MicrosoftExtensionsDependencyInjectionSpecificationTestsPackageVersion> <MicrosoftExtensionsDependencyInjectionPackageVersion>2.1.1</MicrosoftExtensionsDependencyInjectionPackageVersion> + <MicrosoftExtensionsDependencyInjectionSpecificationTestsPackageVersion>2.1.1</MicrosoftExtensionsDependencyInjectionSpecificationTestsPackageVersion> <MicrosoftExtensionsDiagnosticAdapterPackageVersion>2.1.0</MicrosoftExtensionsDiagnosticAdapterPackageVersion> <MicrosoftExtensionsFileProvidersAbstractionsPackageVersion>2.1.1</MicrosoftExtensionsFileProvidersAbstractionsPackageVersion> <MicrosoftExtensionsFileProvidersCompositePackageVersion>2.1.1</MicrosoftExtensionsFileProvidersCompositePackageVersion> @@ -58,6 +58,8 @@ <MicrosoftExtensionsFileProvidersPhysicalPackageVersion>2.1.1</MicrosoftExtensionsFileProvidersPhysicalPackageVersion> <MicrosoftExtensionsFileSystemGlobbingPackageVersion>2.1.1</MicrosoftExtensionsFileSystemGlobbingPackageVersion> <MicrosoftExtensionsHashCodeCombinerSourcesPackageVersion>2.1.1</MicrosoftExtensionsHashCodeCombinerSourcesPackageVersion> + <MicrosoftExtensionsHostingAbstractionsPackageVersion>2.1.1</MicrosoftExtensionsHostingAbstractionsPackageVersion> + <MicrosoftExtensionsHostingPackageVersion>2.1.1</MicrosoftExtensionsHostingPackageVersion> <MicrosoftExtensionsHttpPackageVersion>2.1.1</MicrosoftExtensionsHttpPackageVersion> <MicrosoftExtensionsLoggingAbstractionsPackageVersion>2.1.1</MicrosoftExtensionsLoggingAbstractionsPackageVersion> <MicrosoftExtensionsLoggingAzureAppServicesPackageVersion>2.1.1</MicrosoftExtensionsLoggingAzureAppServicesPackageVersion> @@ -66,9 +68,9 @@ <MicrosoftExtensionsLoggingDebugPackageVersion>2.1.1</MicrosoftExtensionsLoggingDebugPackageVersion> <MicrosoftExtensionsLoggingEventLogPackageVersion>2.1.1</MicrosoftExtensionsLoggingEventLogPackageVersion> <MicrosoftExtensionsLoggingEventSourcePackageVersion>2.1.1</MicrosoftExtensionsLoggingEventSourcePackageVersion> + <MicrosoftExtensionsLoggingPackageVersion>2.1.1</MicrosoftExtensionsLoggingPackageVersion> <MicrosoftExtensionsLoggingTestingPackageVersion>2.1.1</MicrosoftExtensionsLoggingTestingPackageVersion> <MicrosoftExtensionsLoggingTraceSourcePackageVersion>2.1.1</MicrosoftExtensionsLoggingTraceSourcePackageVersion> - <MicrosoftExtensionsLoggingPackageVersion>2.1.1</MicrosoftExtensionsLoggingPackageVersion> <MicrosoftExtensionsObjectMethodExecutorSourcesPackageVersion>2.1.1</MicrosoftExtensionsObjectMethodExecutorSourcesPackageVersion> <MicrosoftExtensionsObjectPoolPackageVersion>2.1.6</MicrosoftExtensionsObjectPoolPackageVersion> <MicrosoftExtensionsOptionsConfigurationExtensionsPackageVersion>2.1.1</MicrosoftExtensionsOptionsConfigurationExtensionsPackageVersion> @@ -83,10 +85,13 @@ <MicrosoftExtensionsStackTraceSourcesPackageVersion>2.1.1</MicrosoftExtensionsStackTraceSourcesPackageVersion> <MicrosoftExtensionsTypeNameHelperSourcesPackageVersion>2.1.1</MicrosoftExtensionsTypeNameHelperSourcesPackageVersion> <MicrosoftExtensionsValueStopwatchSourcesPackageVersion>2.1.1</MicrosoftExtensionsValueStopwatchSourcesPackageVersion> - <MicrosoftExtensionsWebEncodersSourcesPackageVersion>2.1.1</MicrosoftExtensionsWebEncodersSourcesPackageVersion> <MicrosoftExtensionsWebEncodersPackageVersion>2.1.1</MicrosoftExtensionsWebEncodersPackageVersion> + <MicrosoftExtensionsWebEncodersSourcesPackageVersion>2.1.1</MicrosoftExtensionsWebEncodersSourcesPackageVersion> + <!-- These dependencies are temporary while we refactor package refs into project refs. --> <MicrosoftExtensionsBuffersTestingSourcesPackageVersion>2.1.1</MicrosoftExtensionsBuffersTestingSourcesPackageVersion> + <MicrosoftAspNetCoreHostingWebHostBuilderFactorySourcesPackageVersion>2.1.1</MicrosoftAspNetCoreHostingWebHostBuilderFactorySourcesPackageVersion> + <MicrosoftAspNetCoreServerIntegrationTestingPackageVersion>0.5.1</MicrosoftAspNetCoreServerIntegrationTestingPackageVersion> <!-- External and partner dependencies --> <AngleSharpPackageVersion>0.9.9</AngleSharpPackageVersion> diff --git a/build/external-dependencies.props b/build/external-dependencies.props index 32846302dd1ce27bf19f5e2db63117f572d9ccd8..684b4db40b4eaf425a242b3fcb32104f0d4bcb30 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -58,6 +58,8 @@ <ExtensionsDependency Include="Microsoft.Extensions.FileProviders.Physical" Version="$(MicrosoftExtensionsFileProvidersPhysicalPackageVersion)" AllMetapackage="true" AppMetapackage="true" /> <ExtensionsDependency Include="Microsoft.Extensions.FileSystemGlobbing" Version="$(MicrosoftExtensionsFileSystemGlobbingPackageVersion)" AllMetapackage="true" AppMetapackage="true" /> <ExtensionsDependency Include="Microsoft.Extensions.HashCodeCombiner.Sources" Version="$(MicrosoftExtensionsHashCodeCombinerSourcesPackageVersion)" /> + <ExtensionsDependency Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(MicrosoftExtensionsHostingAbstractionsPackageVersion)" AllMetapackage="true" AppMetapackage="true" /> + <ExtensionsDependency Include="Microsoft.Extensions.Hosting" Version="$(MicrosoftExtensionsHostingPackageVersion)" AllMetapackage="true" AppMetapackage="true" /> <ExtensionsDependency Include="Microsoft.Extensions.Http" Version="$(MicrosoftExtensionsHttpPackageVersion)" AllMetapackage="true" AppMetapackage="true" /> <ExtensionsDependency Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" AllMetapackage="true" AppMetapackage="true" /> <ExtensionsDependency Include="Microsoft.Extensions.Logging.AzureAppServices" Version="$(MicrosoftExtensionsLoggingAzureAppServicesPackageVersion)" AllMetapackage="true" /> @@ -86,7 +88,10 @@ <ExtensionsDependency Include="Microsoft.Extensions.WebEncoders.Sources" Version="$(MicrosoftExtensionsWebEncodersSourcesPackageVersion)" /> <ExtensionsDependency Include="Microsoft.Extensions.WebEncoders" Version="$(MicrosoftExtensionsWebEncodersPackageVersion)" AllMetapackage="true" AppMetapackage="true" /> + <!-- These dependencies are temporary while we refactor package refs into project refs. --> <ExtensionsDependency Include="Microsoft.Extensions.Buffers.Testing.Sources" Version="$(MicrosoftExtensionsBuffersTestingSourcesPackageVersion)" /> + <ExtensionsDependency Include="Microsoft.AspNetCore.Hosting.WebHostBuilderFactory.Sources" Version="$(MicrosoftAspNetCoreHostingWebHostBuilderFactorySourcesPackageVersion)" /> + <ExtensionsDependency Include="Microsoft.AspNetCore.Server.IntegrationTesting" Version="$(MicrosoftAspNetCoreServerIntegrationTestingPackageVersion)" /> </ItemGroup> <ItemGroup> diff --git a/build/repo.props b/build/repo.props index 1d5261431b8bcef99c1db924d5881cc42baafa89..a1b4241fbba4498fc2e0fbd0a1a6bab4f67a52b0 100644 --- a/build/repo.props +++ b/build/repo.props @@ -42,7 +42,7 @@ </ItemGroup> <ItemGroup> - <SamplesProject Include="$(RepositoryRoot)src\samples\**\*.csproj;"/> + <SamplesProject Include="$(RepositoryRoot)src\**\samples\**\*.csproj;"/> <ProjectToExclude Include="@(SamplesProject)" Condition="'$(BuildSamples)' == 'false' "/> @@ -55,6 +55,8 @@ <ProjectToBuild Include=" $(RepositoryRoot)src\Features\JsonPatch\**\*.*proj; $(RepositoryRoot)src\DataProtection\**\*.*proj; + $(RepositoryRoot)src\Hosting\**\*.*proj; + $(RepositoryRoot)src\Http\**\*.*proj; $(RepositoryRoot)src\Html\**\*.*proj; $(RepositoryRoot)src\Servers\**\*.*proj; $(RepositoryRoot)src\Tools\**\*.*proj; diff --git a/build/submodules.props b/build/submodules.props index ccc78ab2e8f8758020b7437b6fe9d4facabb8e29..638753659f5c9d426fcb8307de9ade51733f2916 100644 --- a/build/submodules.props +++ b/build/submodules.props @@ -56,8 +56,6 @@ <ShippedRepository Include="CORS" /> <ShippedRepository Include="Diagnostics" /> <ShippedRepository Include="EntityFrameworkCore" /> - <ShippedRepository Include="Hosting" /> - <ShippedRepository Include="HttpAbstractions" /> <ShippedRepository Include="HttpSysServer" /> <ShippedRepository Include="Identity" /> <ShippedRepository Include="JavaScriptServices" RootPath="$(RepositoryRoot)src\JavaScriptServices\" /> diff --git a/eng/Baseline.props b/eng/Baseline.props index a8f9316a92b70a2cfb3e07c2e23d91092db79f51..9d9a2241b278c019ef217f493f01d91dc6d053a5 100644 --- a/eng/Baseline.props +++ b/eng/Baseline.props @@ -24,6 +24,24 @@ <BaselinePackageVersion>2.1.1</BaselinePackageVersion> </PropertyGroup> <ItemGroup Condition=" '$(PackageId)' == 'dotnet-watch' AND '$(TargetFramework)' == 'netcoreapp2.1' " /> + <!-- Package: Microsoft.AspNetCore.Authentication.Abstractions--> + <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Authentication.Abstractions' "> + <BaselinePackageVersion>2.1.1</BaselinePackageVersion> + </PropertyGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Authentication.Abstractions' AND '$(TargetFramework)' == 'netstandard2.0' "> + <BaselinePackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.Options" Version="[2.1.1, )" /> + </ItemGroup> + <!-- Package: Microsoft.AspNetCore.Authentication.Core--> + <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Authentication.Core' "> + <BaselinePackageVersion>2.1.1</BaselinePackageVersion> + </PropertyGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Authentication.Core' AND '$(TargetFramework)' == 'netstandard2.0' "> + <BaselinePackageReference Include="Microsoft.AspNetCore.Authentication.Abstractions" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.AspNetCore.Http" Version="[2.1.1, )" /> + </ItemGroup> <!-- Package: Microsoft.AspNetCore.Connections.Abstractions--> <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Connections.Abstractions' "> <BaselinePackageVersion>2.1.3</BaselinePackageVersion> @@ -108,6 +126,53 @@ <BaselinePackageReference Include="System.Security.Cryptography.Xml" Version="[4.5.0, )" /> <BaselinePackageReference Include="System.Security.Principal.Windows" Version="[4.5.0, )" /> </ItemGroup> + <!-- Package: Microsoft.AspNetCore.Hosting.Abstractions--> + <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Hosting.Abstractions' "> + <BaselinePackageVersion>2.1.1</BaselinePackageVersion> + </PropertyGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Hosting.Abstractions' AND '$(TargetFramework)' == 'netstandard2.0' "> + <BaselinePackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="[2.1.1, )" /> + </ItemGroup> + <!-- Package: Microsoft.AspNetCore.Hosting.Server.Abstractions--> + <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Hosting.Server.Abstractions' "> + <BaselinePackageVersion>2.1.1</BaselinePackageVersion> + </PropertyGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Hosting.Server.Abstractions' AND '$(TargetFramework)' == 'netstandard2.0' "> + <BaselinePackageReference Include="Microsoft.AspNetCore.Http.Features" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="[2.1.1, )" /> + </ItemGroup> + <!-- Package: Microsoft.AspNetCore.Hosting.WindowsServices--> + <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Hosting.WindowsServices' "> + <BaselinePackageVersion>2.1.1</BaselinePackageVersion> + </PropertyGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Hosting.WindowsServices' AND '$(TargetFramework)' == 'net461' "> + <BaselinePackageReference Include="Microsoft.AspNetCore.Hosting" Version="[2.1.1, )" /> + </ItemGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Hosting.WindowsServices' AND '$(TargetFramework)' == 'netstandard2.0' "> + <BaselinePackageReference Include="Microsoft.AspNetCore.Hosting" Version="[2.1.1, )" /> + <BaselinePackageReference Include="System.ServiceProcess.ServiceController" Version="[4.5.0, )" /> + </ItemGroup> + <!-- Package: Microsoft.AspNetCore.Hosting--> + <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Hosting' "> + <BaselinePackageVersion>2.1.1</BaselinePackageVersion> + </PropertyGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Hosting' AND '$(TargetFramework)' == 'netstandard2.0' "> + <BaselinePackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.AspNetCore.Http" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.Configuration" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.DependencyInjection" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.Logging" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.Options" Version="[2.1.1, )" /> + <BaselinePackageReference Include="System.Diagnostics.DiagnosticSource" Version="[4.5.0, )" /> + <BaselinePackageReference Include="System.Reflection.Metadata" Version="[1.6.0, )" /> + </ItemGroup> <!-- Package: Microsoft.AspNetCore.Html.Abstractions--> <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Html.Abstractions' "> <BaselinePackageVersion>2.1.1</BaselinePackageVersion> @@ -115,6 +180,42 @@ <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Html.Abstractions' AND '$(TargetFramework)' == 'netstandard2.0' "> <BaselinePackageReference Include="System.Text.Encodings.Web" Version="[4.5.0, )" /> </ItemGroup> + <!-- Package: Microsoft.AspNetCore.Http.Abstractions--> + <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Http.Abstractions' "> + <BaselinePackageVersion>2.1.1</BaselinePackageVersion> + </PropertyGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Http.Abstractions' AND '$(TargetFramework)' == 'netstandard2.0' "> + <BaselinePackageReference Include="Microsoft.AspNetCore.Http.Features" Version="[2.1.1, )" /> + <BaselinePackageReference Include="System.Text.Encodings.Web" Version="[4.5.0, )" /> + </ItemGroup> + <!-- Package: Microsoft.AspNetCore.Http.Extensions--> + <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Http.Extensions' "> + <BaselinePackageVersion>2.1.1</BaselinePackageVersion> + </PropertyGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Http.Extensions' AND '$(TargetFramework)' == 'netstandard2.0' "> + <BaselinePackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Net.Http.Headers" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.FileProviders.Abstractions" Version="[2.1.1, )" /> + <BaselinePackageReference Include="System.Buffers" Version="[4.5.0, )" /> + </ItemGroup> + <!-- Package: Microsoft.AspNetCore.Http.Features--> + <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Http.Features' "> + <BaselinePackageVersion>2.1.1</BaselinePackageVersion> + </PropertyGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Http.Features' AND '$(TargetFramework)' == 'netstandard2.0' "> + <BaselinePackageReference Include="Microsoft.Extensions.Primitives" Version="[2.1.1, )" /> + </ItemGroup> + <!-- Package: Microsoft.AspNetCore.Http--> + <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Http' "> + <BaselinePackageVersion>2.1.1</BaselinePackageVersion> + </PropertyGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Http' AND '$(TargetFramework)' == 'netstandard2.0' "> + <BaselinePackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Net.Http.Headers" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.ObjectPool" Version="[2.1.1, )" /> + <BaselinePackageReference Include="Microsoft.Extensions.Options" Version="[2.1.1, )" /> + </ItemGroup> <!-- Package: Microsoft.AspNetCore.JsonPatch--> <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.JsonPatch' "> <BaselinePackageVersion>2.1.1</BaselinePackageVersion> @@ -123,6 +224,13 @@ <BaselinePackageReference Include="Microsoft.CSharp" Version="[4.5.0, )" /> <BaselinePackageReference Include="Newtonsoft.Json" Version="[11.0.2, )" /> </ItemGroup> + <!-- Package: Microsoft.AspNetCore.Owin--> + <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Owin' "> + <BaselinePackageVersion>2.1.1</BaselinePackageVersion> + </PropertyGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Owin' AND '$(TargetFramework)' == 'netstandard2.0' "> + <BaselinePackageReference Include="Microsoft.AspNetCore.Http" Version="[2.1.1, )" /> + </ItemGroup> <!-- Package: Microsoft.AspNetCore.Server.Kestrel.Core--> <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Server.Kestrel.Core' "> <BaselinePackageVersion>2.1.3</BaselinePackageVersion> @@ -209,6 +317,14 @@ <BaselinePackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" Version="[2.1.3, )" /> <BaselinePackageReference Include="Microsoft.AspNetCore.Hosting" Version="[2.1.1, )" /> </ItemGroup> + <!-- Package: Microsoft.AspNetCore.TestHost--> + <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.TestHost' "> + <BaselinePackageVersion>2.1.1</BaselinePackageVersion> + </PropertyGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.TestHost' AND '$(TargetFramework)' == 'netstandard2.0' "> + <BaselinePackageReference Include="Microsoft.AspNetCore.Hosting" Version="[2.1.1, )" /> + <BaselinePackageReference Include="System.IO.Pipelines" Version="[4.5.0, )" /> + </ItemGroup> <!-- Package: Microsoft.AspNetCore.WebSockets--> <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.WebSockets' "> <BaselinePackageVersion>2.1.1</BaselinePackageVersion> @@ -218,4 +334,20 @@ <BaselinePackageReference Include="Microsoft.Extensions.Options" Version="[2.1.1, )" /> <BaselinePackageReference Include="System.Net.WebSockets.WebSocketProtocol" Version="[4.5.1, )" /> </ItemGroup> + <!-- Package: Microsoft.AspNetCore.WebUtilities--> + <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.WebUtilities' "> + <BaselinePackageVersion>2.1.1</BaselinePackageVersion> + </PropertyGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.WebUtilities' AND '$(TargetFramework)' == 'netstandard2.0' "> + <BaselinePackageReference Include="Microsoft.Net.Http.Headers" Version="[2.1.1, )" /> + <BaselinePackageReference Include="System.Text.Encodings.Web" Version="[4.5.0, )" /> + </ItemGroup> + <!-- Package: Microsoft.Net.Http.Headers--> + <PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.Net.Http.Headers' "> + <BaselinePackageVersion>2.1.1</BaselinePackageVersion> + </PropertyGroup> + <ItemGroup Condition=" '$(PackageId)' == 'Microsoft.Net.Http.Headers' AND '$(TargetFramework)' == 'netstandard2.0' "> + <BaselinePackageReference Include="Microsoft.Extensions.Primitives" Version="[2.1.1, )" /> + <BaselinePackageReference Include="System.Buffers" Version="[4.5.0, )" /> + </ItemGroup> </Project> \ No newline at end of file diff --git a/eng/Dependencies.props b/eng/Dependencies.props index c2792fb38420bd9dc94e6e2b945178ec39d02dcd..0ad0e061110d0f3bd130c0acc27c996dfccfed4e 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -15,19 +15,28 @@ <LatestPackageReference Include="Microsoft.CSharp" Version="$(MicrosoftCSharpPackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.ActivatorUtilities.Sources" Version="$(MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.ClosedGenericMatcher.Sources" Version="$(MicrosoftExtensionsClosedGenericMatcherSourcesPackageVersion)" /> + <LatestPackageReference Include="Microsoft.Extensions.CopyOnWriteDictionary.Sources" Version="$(MicrosoftExtensionsCopyOnWriteDictionarySourcesPackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.CommandLineUtils.Sources" Version="$(MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="$(MicrosoftExtensionsConfigurationCommandLinePackageVersion)" /> + <LatestPackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="$(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftExtensionsConfigurationJsonPackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="$(MicrosoftExtensionsConfigurationUserSecretsPackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.Configuration" Version="$(MicrosoftExtensionsConfigurationPackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" /> + <LatestPackageReference Include="Microsoft.Extensions.DiagnosticAdapter" Version="$(MicrosoftExtensionsDiagnosticAdapterPackageVersion)" /> + <LatestPackageReference Include="Microsoft.Extensions.Hosting" Version="$(MicrosoftExtensionsHostingPackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingPackageVersion)" /> + <LatestPackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="$(MicrosoftExtensionsFileProvidersEmbeddedPackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.Process.Sources" Version="$(MicrosoftExtensionsProcessSourcesPackageVersion)" /> + <LatestPackageReference Include="Microsoft.Extensions.RazorViews.Sources" Version="$(MicrosoftExtensionsRazorViewsSourcesPackageVersion)" /> + <LatestPackageReference Include="Microsoft.Extensions.StackTrace.Sources" Version="$(MicrosoftExtensionsStackTraceSourcesPackageVersion)" /> + <LatestPackageReference Include="Microsoft.Extensions.TypeNameHelper.Sources" Version="$(MicrosoftExtensionsTypeNameHelperSourcesPackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.WebEncoders.Sources" Version="$(MicrosoftExtensionsWebEncodersSourcesPackageVersion)" /> <LatestPackageReference Include="Microsoft.Extensions.WebEncoders" Version="$(MicrosoftExtensionsWebEncodersPackageVersion)" /> + <LatestPackageReference Include="Microsoft.NETCore.Windows.ApiSets" Version="$(MicrosoftNETCoreWindowsApiSetsPackageVersion)" /> <LatestPackageReference Include="System.Data.SqlClient" Version="$(SystemDataSqlClientPackageVersion)" /> <LatestPackageReference Include="System.Memory" Version="$(SystemMemoryPackageVersion)" /> <LatestPackageReference Include="System.Net.WebSockets.WebSocketProtocol" Version="$(SystemNetWebSocketsWebSocketProtocolPackageVersion)" /> @@ -44,6 +53,8 @@ <LatestPackageReference Include="Newtonsoft.Json" Version="9.0.1" Condition="'$(UseMSBuildJsonNet)' == 'true'" /> <!-- This version should be used by runtime packages --> <LatestPackageReference Include="Newtonsoft.Json" Version="11.0.2" Condition="'$(UseMSBuildJsonNet)' != 'true'" /> + <LatestPackageReference Include="Serilog.Extensions.Logging" Version="$(SerilogExtensionsLoggingPackageVersion)" /> + <LatestPackageReference Include="Serilog.Sinks.File" Version="$(SerilogSinksFilePackageVersion)" /> <LatestPackageReference Include="Utf8Json" Version="1.3.7" /> <LatestPackageReference Include="xunit.abstractions" Version="2.0.1" /> <LatestPackageReference Include="xunit.analyzers" Version="0.10.0" /> diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 7d04ed99b8fbe940afc0a4bc4f2bc354b5ee970f..0e25409b66ce89682bc1b7ff3d7cae686860752a 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -11,6 +11,21 @@ <ProjectReferenceProvider Include="Microsoft.AspNetCore.DataProtection.Extensions" ProjectPath="$(RepositoryRoot)src\DataProtection\Extensions\src\Microsoft.AspNetCore.DataProtection.Extensions.csproj" /> <ProjectReferenceProvider Include="Microsoft.AspNetCore.DataProtection.Redis" ProjectPath="$(RepositoryRoot)src\DataProtection\Redis\src\Microsoft.AspNetCore.DataProtection.Redis.csproj" /> <ProjectReferenceProvider Include="Microsoft.AspNetCore.DataProtection.SystemWeb" ProjectPath="$(RepositoryRoot)src\DataProtection\SystemWeb\src\Microsoft.AspNetCore.DataProtection.SystemWeb.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.Hosting.Abstractions" ProjectPath="$(RepositoryRoot)src\Hosting\Abstractions\src\Microsoft.AspNetCore.Hosting.Abstractions.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.Hosting" ProjectPath="$(RepositoryRoot)src\Hosting\Hosting\src\Microsoft.AspNetCore.Hosting.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" ProjectPath="$(RepositoryRoot)src\Hosting\Server.Abstractions\src\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.IntegrationTesting" ProjectPath="$(RepositoryRoot)src\Hosting\Server.IntegrationTesting\src\Microsoft.AspNetCore.Server.IntegrationTesting.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.TestHost" ProjectPath="$(RepositoryRoot)src\Hosting\TestHost\src\Microsoft.AspNetCore.TestHost.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.Hosting.WindowsServices" ProjectPath="$(RepositoryRoot)src\Hosting\WindowsServices\src\Microsoft.AspNetCore.Hosting.WindowsServices.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Abstractions" ProjectPath="$(RepositoryRoot)src\Http\Authentication.Abstractions\src\Microsoft.AspNetCore.Authentication.Abstractions.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Core" ProjectPath="$(RepositoryRoot)src\Http\Authentication.Core\src\Microsoft.AspNetCore.Authentication.Core.csproj" /> + <ProjectReferenceProvider Include="Microsoft.Net.Http.Headers" ProjectPath="$(RepositoryRoot)src\Http\Headers\src\Microsoft.Net.Http.Headers.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.Http.Abstractions" ProjectPath="$(RepositoryRoot)src\Http\Http.Abstractions\src\Microsoft.AspNetCore.Http.Abstractions.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.Http.Extensions" ProjectPath="$(RepositoryRoot)src\Http\Http.Extensions\src\Microsoft.AspNetCore.Http.Extensions.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.Http.Features" ProjectPath="$(RepositoryRoot)src\Http\Http.Features\src\Microsoft.AspNetCore.Http.Features.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.Http" ProjectPath="$(RepositoryRoot)src\Http\Http\src\Microsoft.AspNetCore.Http.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.Owin" ProjectPath="$(RepositoryRoot)src\Http\Owin\src\Microsoft.AspNetCore.Owin.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.WebUtilities" ProjectPath="$(RepositoryRoot)src\Http\WebUtilities\src\Microsoft.AspNetCore.WebUtilities.csproj" /> <ProjectReferenceProvider Include="Microsoft.AspNetCore.Html.Abstractions" ProjectPath="$(RepositoryRoot)src\Html\Abstractions\src\Microsoft.AspNetCore.Html.Abstractions.csproj" /> <ProjectReferenceProvider Include="Microsoft.AspNetCore.Connections.Abstractions" ProjectPath="$(RepositoryRoot)src\Servers\Connections.Abstractions\src\Microsoft.AspNetCore.Connections.Abstractions.csproj" /> <ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Core" ProjectPath="$(RepositoryRoot)src\Servers\Kestrel\Core\src\Microsoft.AspNetCore.Server.Kestrel.Core.csproj" /> diff --git a/eng/dependencies.temp.props b/eng/dependencies.temp.props index a96067429f096ffde50b8b1cd31f14014ca0f4c0..f319c9348c3a1bb0107bac8266afa0702ad110db 100644 --- a/eng/dependencies.temp.props +++ b/eng/dependencies.temp.props @@ -1,5 +1,5 @@ <!-- -This file is temporary until aspnet/Hosting, Diagnostics, StaticFiles, and HttpAbstractions are merged into this repo. +This file is temporary until aspnet/Diagnostics and StaticFiles are merged into this repo. This is required to provide dependencies for samples and tests. --> <Project> @@ -7,9 +7,5 @@ This is required to provide dependencies for samples and tests. <LatestPackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="2.1.1" /> <LatestPackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="2.1.1" /> <LatestPackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.1" /> - <LatestPackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.1.1" /> - <LatestPackageReference Include="Microsoft.AspNetCore.Http.Features" Version="2.1.1" /> - <LatestPackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.1.1" /> - <LatestPackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.1" /> </ItemGroup> </Project> diff --git a/eng/tools/BaselineGenerator/baseline.xml b/eng/tools/BaselineGenerator/baseline.xml index 823c131a302693f2903a3dfb5a180734bdcbb5c2..5a3e9661b96d372ce379b6baf7967e0f9729bbf8 100644 --- a/eng/tools/BaselineGenerator/baseline.xml +++ b/eng/tools/BaselineGenerator/baseline.xml @@ -3,6 +3,8 @@ <Package Id="dotnet-sql-cache" Version="2.1.1" /> <Package Id="dotnet-user-secrets" Version="2.1.1" /> <Package Id="dotnet-watch" Version="2.1.1" /> + <Package Id="Microsoft.AspNetCore.Authentication.Abstractions" Version="2.1.1" /> + <Package Id="Microsoft.AspNetCore.Authentication.Core" Version="2.1.1" /> <Package Id="Microsoft.AspNetCore.Connections.Abstractions" Version="2.1.3" /> <Package Id="Microsoft.AspNetCore.Cryptography.Internal" Version="2.1.1" /> <Package Id="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="2.1.1" /> @@ -13,13 +15,25 @@ <Package Id="Microsoft.AspNetCore.DataProtection.Redis" Version="0.4.1" /> <Package Id="Microsoft.AspNetCore.DataProtection.SystemWeb" Version="2.1.1" /> <Package Id="Microsoft.AspNetCore.DataProtection" Version="2.1.1" /> + <Package Id="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.1.1" /> + <Package Id="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.1.1" /> + <Package Id="Microsoft.AspNetCore.Hosting.WindowsServices" Version="2.1.1" /> + <Package Id="Microsoft.AspNetCore.Hosting" Version="2.1.1" /> <Package Id="Microsoft.AspNetCore.Html.Abstractions" Version="2.1.1" /> + <Package Id="Microsoft.AspNetCore.Http.Abstractions" Version="2.1.1" /> + <Package Id="Microsoft.AspNetCore.Http.Extensions" Version="2.1.1" /> + <Package Id="Microsoft.AspNetCore.Http.Features" Version="2.1.1" /> + <Package Id="Microsoft.AspNetCore.Http" Version="2.1.1" /> <Package Id="Microsoft.AspNetCore.JsonPatch" Version="2.1.1" /> + <Package Id="Microsoft.AspNetCore.Owin" Version="2.1.1" /> <Package Id="Microsoft.AspNetCore.Server.Kestrel.Core" Version="2.1.3" /> <Package Id="Microsoft.AspNetCore.Server.Kestrel.Https" Version="2.1.3" /> <Package Id="Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions" Version="2.1.3" /> <Package Id="Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" Version="2.1.3" /> <Package Id="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" Version="2.1.3" /> <Package Id="Microsoft.AspNetCore.Server.Kestrel" Version="2.1.3" /> + <Package Id="Microsoft.AspNetCore.TestHost" Version="2.1.1" /> <Package Id="Microsoft.AspNetCore.WebSockets" Version="2.1.1" /> + <Package Id="Microsoft.AspNetCore.WebUtilities" Version="2.1.1" /> + <Package Id="Microsoft.Net.Http.Headers" Version="2.1.1" /> </Baseline> diff --git a/modules/Hosting b/modules/Hosting deleted file mode 160000 index 3f7ee338d4cdd1c49bb965338ad7b118fa070a83..0000000000000000000000000000000000000000 --- a/modules/Hosting +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3f7ee338d4cdd1c49bb965338ad7b118fa070a83 diff --git a/modules/HttpAbstractions b/modules/HttpAbstractions deleted file mode 160000 index d142d58eb43626961117136c51993d51dfb7371d..0000000000000000000000000000000000000000 --- a/modules/HttpAbstractions +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d142d58eb43626961117136c51993d51dfb7371d diff --git a/src/Hosting/Abstractions/src/EnvironmentName.cs b/src/Hosting/Abstractions/src/EnvironmentName.cs new file mode 100644 index 0000000000000000000000000000000000000000..d5522d11240ed7a9088b1573b0feb07ba8c36d7e --- /dev/null +++ b/src/Hosting/Abstractions/src/EnvironmentName.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Hosting +{ + /// <summary> + /// Commonly used environment names. + /// </summary> + public static class EnvironmentName + { + public static readonly string Development = "Development"; + public static readonly string Staging = "Staging"; + public static readonly string Production = "Production"; + } +} \ No newline at end of file diff --git a/src/Hosting/Abstractions/src/HostingAbstractionsWebHostBuilderExtensions.cs b/src/Hosting/Abstractions/src/HostingAbstractionsWebHostBuilderExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..f61b86eb862648cf702c0d3086853ef62a42e1bc --- /dev/null +++ b/src/Hosting/Abstractions/src/HostingAbstractionsWebHostBuilderExtensions.cs @@ -0,0 +1,197 @@ +// 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.Globalization; +using System.Linq; +using System.Threading; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting +{ + public static class HostingAbstractionsWebHostBuilderExtensions + { + private static readonly string ServerUrlsSeparator = ";"; + + /// <summary> + /// Use the given configuration settings on the web host. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="configuration">The <see cref="IConfiguration"/> containing settings to be used.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder UseConfiguration(this IWebHostBuilder hostBuilder, IConfiguration configuration) + { + foreach (var setting in configuration.AsEnumerable()) + { + hostBuilder.UseSetting(setting.Key, setting.Value); + } + + return hostBuilder; + } + + /// <summary> + /// Set whether startup errors should be captured in the configuration settings of the web host. + /// When enabled, startup exceptions will be caught and an error page will be returned. If disabled, startup exceptions will be propagated. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="captureStartupErrors"><c>true</c> to use startup error page; otherwise <c>false</c>.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder CaptureStartupErrors(this IWebHostBuilder hostBuilder, bool captureStartupErrors) + { + return hostBuilder.UseSetting(WebHostDefaults.CaptureStartupErrorsKey, captureStartupErrors ? "true" : "false"); + } + + /// <summary> + /// Specify the assembly containing the startup type to be used by the web host. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="startupAssemblyName">The name of the assembly containing the startup type.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, string startupAssemblyName) + { + if (startupAssemblyName == null) + { + throw new ArgumentNullException(nameof(startupAssemblyName)); + } + + + return hostBuilder + .UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName) + .UseSetting(WebHostDefaults.StartupAssemblyKey, startupAssemblyName); + } + + /// <summary> + /// Specify the server to be used by the web host. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="server">The <see cref="IServer"/> to be used.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder UseServer(this IWebHostBuilder hostBuilder, IServer server) + { + if (server == null) + { + throw new ArgumentNullException(nameof(server)); + } + + return hostBuilder.ConfigureServices(services => + { + // It would be nicer if this was transient but we need to pass in the + // factory instance directly + services.AddSingleton(server); + }); + } + + /// <summary> + /// Specify the environment to be used by the web host. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="environment">The environment to host the application in.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder UseEnvironment(this IWebHostBuilder hostBuilder, string environment) + { + if (environment == null) + { + throw new ArgumentNullException(nameof(environment)); + } + + return hostBuilder.UseSetting(WebHostDefaults.EnvironmentKey, environment); + } + + /// <summary> + /// Specify the content root directory to be used by the web host. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="contentRoot">Path to root directory of the application.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder UseContentRoot(this IWebHostBuilder hostBuilder, string contentRoot) + { + if (contentRoot == null) + { + throw new ArgumentNullException(nameof(contentRoot)); + } + + return hostBuilder.UseSetting(WebHostDefaults.ContentRootKey, contentRoot); + } + + /// <summary> + /// Specify the webroot directory to be used by the web host. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="webRoot">Path to the root directory used by the web server.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder UseWebRoot(this IWebHostBuilder hostBuilder, string webRoot) + { + if (webRoot == null) + { + throw new ArgumentNullException(nameof(webRoot)); + } + + return hostBuilder.UseSetting(WebHostDefaults.WebRootKey, webRoot); + } + + /// <summary> + /// Specify the urls the web host will listen on. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="urls">The urls the hosted application will listen on.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder UseUrls(this IWebHostBuilder hostBuilder, params string[] urls) + { + if (urls == null) + { + throw new ArgumentNullException(nameof(urls)); + } + + return hostBuilder.UseSetting(WebHostDefaults.ServerUrlsKey, string.Join(ServerUrlsSeparator, urls)); + } + + /// <summary> + /// Indicate whether the host should listen on the URLs configured on the <see cref="IWebHostBuilder"/> + /// instead of those configured on the <see cref="IServer"/>. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="preferHostingUrls"><c>true</c> to prefer URLs configured on the <see cref="IWebHostBuilder"/>; otherwise <c>false</c>.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder PreferHostingUrls(this IWebHostBuilder hostBuilder, bool preferHostingUrls) + { + return hostBuilder.UseSetting(WebHostDefaults.PreferHostingUrlsKey, preferHostingUrls ? "true" : "false"); + } + + /// <summary> + /// Specify if startup status messages should be suppressed. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="suppressStatusMessages"><c>true</c> to suppress writing of hosting startup status messages; otherwise <c>false</c>.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder SuppressStatusMessages(this IWebHostBuilder hostBuilder, bool suppressStatusMessages) + { + return hostBuilder.UseSetting(WebHostDefaults.SuppressStatusMessagesKey, suppressStatusMessages ? "true" : "false"); + } + + /// <summary> + /// Specify the amount of time to wait for the web host to shutdown. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="timeout">The amount of time to wait for server shutdown.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder UseShutdownTimeout(this IWebHostBuilder hostBuilder, TimeSpan timeout) + { + return hostBuilder.UseSetting(WebHostDefaults.ShutdownTimeoutKey, ((int)timeout.TotalSeconds).ToString(CultureInfo.InvariantCulture)); + } + + /// <summary> + /// Start the web host and listen on the specified urls. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to start.</param> + /// <param name="urls">The urls the hosted application will listen on.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHost Start(this IWebHostBuilder hostBuilder, params string[] urls) + { + var host = hostBuilder.UseUrls(urls).Build(); + host.StartAsync(CancellationToken.None).GetAwaiter().GetResult(); + return host; + } + } +} diff --git a/src/Hosting/Abstractions/src/HostingEnvironmentExtensions.cs b/src/Hosting/Abstractions/src/HostingEnvironmentExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..ad3269859da579727c55f9d9c23dfb364d5667da --- /dev/null +++ b/src/Hosting/Abstractions/src/HostingEnvironmentExtensions.cs @@ -0,0 +1,80 @@ +// 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; + +namespace Microsoft.AspNetCore.Hosting +{ + /// <summary> + /// Extension methods for <see cref="IHostingEnvironment"/>. + /// </summary> + public static class HostingEnvironmentExtensions + { + /// <summary> + /// Checks if the current hosting environment name is <see cref="EnvironmentName.Development"/>. + /// </summary> + /// <param name="hostingEnvironment">An instance of <see cref="IHostingEnvironment"/>.</param> + /// <returns>True if the environment name is <see cref="EnvironmentName.Development"/>, otherwise false.</returns> + public static bool IsDevelopment(this IHostingEnvironment hostingEnvironment) + { + if (hostingEnvironment == null) + { + throw new ArgumentNullException(nameof(hostingEnvironment)); + } + + return hostingEnvironment.IsEnvironment(EnvironmentName.Development); + } + + /// <summary> + /// Checks if the current hosting environment name is <see cref="EnvironmentName.Staging"/>. + /// </summary> + /// <param name="hostingEnvironment">An instance of <see cref="IHostingEnvironment"/>.</param> + /// <returns>True if the environment name is <see cref="EnvironmentName.Staging"/>, otherwise false.</returns> + public static bool IsStaging(this IHostingEnvironment hostingEnvironment) + { + if (hostingEnvironment == null) + { + throw new ArgumentNullException(nameof(hostingEnvironment)); + } + + return hostingEnvironment.IsEnvironment(EnvironmentName.Staging); + } + + /// <summary> + /// Checks if the current hosting environment name is <see cref="EnvironmentName.Production"/>. + /// </summary> + /// <param name="hostingEnvironment">An instance of <see cref="IHostingEnvironment"/>.</param> + /// <returns>True if the environment name is <see cref="EnvironmentName.Production"/>, otherwise false.</returns> + public static bool IsProduction(this IHostingEnvironment hostingEnvironment) + { + if (hostingEnvironment == null) + { + throw new ArgumentNullException(nameof(hostingEnvironment)); + } + + return hostingEnvironment.IsEnvironment(EnvironmentName.Production); + } + + /// <summary> + /// Compares the current hosting environment name against the specified value. + /// </summary> + /// <param name="hostingEnvironment">An instance of <see cref="IHostingEnvironment"/>.</param> + /// <param name="environmentName">Environment name to validate against.</param> + /// <returns>True if the specified name is the same as the current environment, otherwise false.</returns> + public static bool IsEnvironment( + this IHostingEnvironment hostingEnvironment, + string environmentName) + { + if (hostingEnvironment == null) + { + throw new ArgumentNullException(nameof(hostingEnvironment)); + } + + return string.Equals( + hostingEnvironment.EnvironmentName, + environmentName, + StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/Hosting/Abstractions/src/HostingStartupAttribute.cs b/src/Hosting/Abstractions/src/HostingStartupAttribute.cs new file mode 100644 index 0000000000000000000000000000000000000000..cb028c327b5c030b38a7632301a7f37acd5f96d9 --- /dev/null +++ b/src/Hosting/Abstractions/src/HostingStartupAttribute.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; + +namespace Microsoft.AspNetCore.Hosting +{ + /// <summary> + /// Marker attribute indicating an implementation of <see cref="IHostingStartup"/> that will be loaded and executed when building an <see cref="IWebHost"/>. + /// </summary> + [AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)] + public sealed class HostingStartupAttribute : Attribute + { + /// <summary> + /// Constructs the <see cref="HostingStartupAttribute"/> with the specified type. + /// </summary> + /// <param name="hostingStartupType">A type that implements <see cref="IHostingStartup"/>.</param> + public HostingStartupAttribute(Type hostingStartupType) + { + if (hostingStartupType == null) + { + throw new ArgumentNullException(nameof(hostingStartupType)); + } + + if (!typeof(IHostingStartup).GetTypeInfo().IsAssignableFrom(hostingStartupType.GetTypeInfo())) + { + throw new ArgumentException($@"""{hostingStartupType}"" does not implement {typeof(IHostingStartup)}.", nameof(hostingStartupType)); + } + + HostingStartupType = hostingStartupType; + } + + /// <summary> + /// The implementation of <see cref="IHostingStartup"/> that should be loaded when + /// starting an application. + /// </summary> + public Type HostingStartupType { get; } + } +} \ No newline at end of file diff --git a/src/Hosting/Abstractions/src/IApplicationLifetime.cs b/src/Hosting/Abstractions/src/IApplicationLifetime.cs new file mode 100644 index 0000000000000000000000000000000000000000..f4613dd7d921d8da1b6627c9cbc3c588fa64aad1 --- /dev/null +++ b/src/Hosting/Abstractions/src/IApplicationLifetime.cs @@ -0,0 +1,37 @@ +// 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; + +namespace Microsoft.AspNetCore.Hosting +{ + /// <summary> + /// Allows consumers to perform cleanup during a graceful shutdown. + /// </summary> + public interface IApplicationLifetime + { + /// <summary> + /// Triggered when the application host has fully started and is about to wait + /// for a graceful shutdown. + /// </summary> + CancellationToken ApplicationStarted { get; } + + /// <summary> + /// Triggered when the application host is performing a graceful shutdown. + /// Requests may still be in flight. Shutdown will block until this event completes. + /// </summary> + CancellationToken ApplicationStopping { get; } + + /// <summary> + /// Triggered when the application host is performing a graceful shutdown. + /// All requests should be complete at this point. Shutdown will block + /// until this event completes. + /// </summary> + CancellationToken ApplicationStopped { get; } + + /// <summary> + /// Requests termination of the current application. + /// </summary> + void StopApplication(); + } +} diff --git a/src/Hosting/Abstractions/src/IHostingEnvironment.cs b/src/Hosting/Abstractions/src/IHostingEnvironment.cs new file mode 100644 index 0000000000000000000000000000000000000000..5feeb38eb7e65f1c775a6e3c770f752b4fca6644 --- /dev/null +++ b/src/Hosting/Abstractions/src/IHostingEnvironment.cs @@ -0,0 +1,45 @@ +// 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.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Hosting +{ + /// <summary> + /// Provides information about the web hosting environment an application is running in. + /// </summary> + public interface IHostingEnvironment + { + /// <summary> + /// Gets or sets the name of the environment. The host automatically sets this property to the value + /// of the "ASPNETCORE_ENVIRONMENT" environment variable, or "environment" as specified in any other configuration source. + /// </summary> + string EnvironmentName { get; set; } + + /// <summary> + /// Gets or sets the name of the application. This property is automatically set by the host to the assembly containing + /// the application entry point. + /// </summary> + string ApplicationName { get; set; } + + /// <summary> + /// Gets or sets the absolute path to the directory that contains the web-servable application content files. + /// </summary> + string WebRootPath { get; set; } + + /// <summary> + /// Gets or sets an <see cref="IFileProvider"/> pointing at <see cref="WebRootPath"/>. + /// </summary> + IFileProvider WebRootFileProvider { get; set; } + + /// <summary> + /// Gets or sets the absolute path to the directory that contains the application content files. + /// </summary> + string ContentRootPath { get; set; } + + /// <summary> + /// Gets or sets an <see cref="IFileProvider"/> pointing at <see cref="ContentRootPath"/>. + /// </summary> + IFileProvider ContentRootFileProvider { get; set; } + } +} diff --git a/src/Hosting/Abstractions/src/IHostingStartup.cs b/src/Hosting/Abstractions/src/IHostingStartup.cs new file mode 100644 index 0000000000000000000000000000000000000000..e65ed18fb63fe6ef2852e255d7f81e5cc0ccd341 --- /dev/null +++ b/src/Hosting/Abstractions/src/IHostingStartup.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Hosting +{ + /// <summary> + /// Represents platform specific configuration that will be applied to a <see cref="IWebHostBuilder"/> when building an <see cref="IWebHost"/>. + /// </summary> + public interface IHostingStartup + { + /// <summary> + /// Configure the <see cref="IWebHostBuilder"/>. + /// </summary> + /// <remarks> + /// Configure is intended to be called before user code, allowing a user to overwrite any changes made. + /// </remarks> + /// <param name="builder"></param> + void Configure(IWebHostBuilder builder); + } +} \ No newline at end of file diff --git a/src/Hosting/Abstractions/src/IStartup.cs b/src/Hosting/Abstractions/src/IStartup.cs new file mode 100644 index 0000000000000000000000000000000000000000..3a533c8df2a7da5e1699ceb7921f0bbadc668d51 --- /dev/null +++ b/src/Hosting/Abstractions/src/IStartup.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting +{ + public interface IStartup + { + IServiceProvider ConfigureServices(IServiceCollection services); + + void Configure(IApplicationBuilder app); + } +} \ No newline at end of file diff --git a/src/Hosting/Abstractions/src/IStartupFilter.cs b/src/Hosting/Abstractions/src/IStartupFilter.cs new file mode 100644 index 0000000000000000000000000000000000000000..2f0a3cf39d3adfd09c13cc119a13871b4aba8d59 --- /dev/null +++ b/src/Hosting/Abstractions/src/IStartupFilter.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Hosting +{ + public interface IStartupFilter + { + Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next); + } +} diff --git a/src/Hosting/Abstractions/src/IWebHost.cs b/src/Hosting/Abstractions/src/IWebHost.cs new file mode 100644 index 0000000000000000000000000000000000000000..97331e47680bdf6ea94ddf72d1e4b9dd4db9b9e5 --- /dev/null +++ b/src/Hosting/Abstractions/src/IWebHost.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Hosting +{ + /// <summary> + /// Represents a configured web host. + /// </summary> + public interface IWebHost : IDisposable + { + /// <summary> + /// The <see cref="IFeatureCollection"/> exposed by the configured server. + /// </summary> + IFeatureCollection ServerFeatures { get; } + + /// <summary> + /// The <see cref="IServiceProvider"/> for the host. + /// </summary> + IServiceProvider Services { get; } + + /// <summary> + /// Starts listening on the configured addresses. + /// </summary> + void Start(); + + /// <summary> + /// Starts listening on the configured addresses. + /// </summary> + Task StartAsync(CancellationToken cancellationToken = default); + + /// <summary> + /// Attempt to gracefully stop the host. + /// </summary> + /// <param name="cancellationToken"></param> + /// <returns></returns> + Task StopAsync(CancellationToken cancellationToken = default); + } +} diff --git a/src/Hosting/Abstractions/src/IWebHostBuilder.cs b/src/Hosting/Abstractions/src/IWebHostBuilder.cs new file mode 100644 index 0000000000000000000000000000000000000000..2cf3bc116327d7037cff6ddbc600429410b4d937 --- /dev/null +++ b/src/Hosting/Abstractions/src/IWebHostBuilder.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Hosting +{ + /// <summary> + /// A builder for <see cref="IWebHost"/>. + /// </summary> + public interface IWebHostBuilder + { + /// <summary> + /// Builds an <see cref="IWebHost"/> which hosts a web application. + /// </summary> + IWebHost Build(); + + /// <summary> + /// Adds a delegate for configuring the <see cref="IConfigurationBuilder"/> that will construct an <see cref="IConfiguration"/>. + /// </summary> + /// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder" /> that will be used to construct an <see cref="IConfiguration" />.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + /// <remarks> + /// The <see cref="IConfiguration"/> and <see cref="ILoggerFactory"/> on the <see cref="WebHostBuilderContext"/> are uninitialized at this stage. + /// The <see cref="IConfigurationBuilder"/> is pre-populated with the settings of the <see cref="IWebHostBuilder"/>. + /// </remarks> + IWebHostBuilder ConfigureAppConfiguration(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate); + + /// <summary> + /// Adds a delegate for configuring additional services for the host or web application. This may be called + /// multiple times. + /// </summary> + /// <param name="configureServices">A delegate for configuring the <see cref="IServiceCollection"/>.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices); + + /// <summary> + /// Adds a delegate for configuring additional services for the host or web application. This may be called + /// multiple times. + /// </summary> + /// <param name="configureServices">A delegate for configuring the <see cref="IServiceCollection"/>.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + IWebHostBuilder ConfigureServices(Action<WebHostBuilderContext, IServiceCollection> configureServices); + + /// <summary> + /// Get the setting value from the configuration. + /// </summary> + /// <param name="key">The key of the setting to look up.</param> + /// <returns>The value the setting currently contains.</returns> + string GetSetting(string key); + + /// <summary> + /// Add or replace a setting in the configuration. + /// </summary> + /// <param name="key">The key of the setting to add or replace.</param> + /// <param name="value">The value of the setting to add or replace.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + IWebHostBuilder UseSetting(string key, string value); + } +} \ No newline at end of file diff --git a/src/Hosting/Abstractions/src/Internal/IStartupConfigureContainerFilter.cs b/src/Hosting/Abstractions/src/Internal/IStartupConfigureContainerFilter.cs new file mode 100644 index 0000000000000000000000000000000000000000..e58ac19774deae4f5715bb5d91ea278660fd55b2 --- /dev/null +++ b/src/Hosting/Abstractions/src/Internal/IStartupConfigureContainerFilter.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + /// <summary> + /// This API supports the ASP.NET Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// </summary> + public interface IStartupConfigureContainerFilter<TContainerBuilder> + { + Action<TContainerBuilder> ConfigureContainer(Action<TContainerBuilder> container); + } +} diff --git a/src/Hosting/Abstractions/src/Internal/IStartupConfigureServicesFilter.cs b/src/Hosting/Abstractions/src/Internal/IStartupConfigureServicesFilter.cs new file mode 100644 index 0000000000000000000000000000000000000000..ad203bcedb6078fe0197290722461f541de4af1f --- /dev/null +++ b/src/Hosting/Abstractions/src/Internal/IStartupConfigureServicesFilter.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + /// <summary> + /// This API supports the ASP.NET Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// </summary> + public interface IStartupConfigureServicesFilter + { + Action<IServiceCollection> ConfigureServices(Action<IServiceCollection> next); + } +} diff --git a/src/Hosting/Abstractions/src/Microsoft.AspNetCore.Hosting.Abstractions.csproj b/src/Hosting/Abstractions/src/Microsoft.AspNetCore.Hosting.Abstractions.csproj new file mode 100644 index 0000000000000000000000000000000000000000..a01be8ea3f16085774adeee2d12bf35d9ae5c533 --- /dev/null +++ b/src/Hosting/Abstractions/src/Microsoft.AspNetCore.Hosting.Abstractions.csproj @@ -0,0 +1,17 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>ASP.NET Core hosting and startup abstractions for web applications.</Description> + <TargetFramework>netstandard2.0</TargetFramework> + <NoWarn>$(NoWarn);CS1591</NoWarn> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore;hosting</PackageTags> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" /> + <Reference Include="Microsoft.AspNetCore.Http.Abstractions" /> + <Reference Include="Microsoft.Extensions.Hosting.Abstractions" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/Abstractions/src/WebHostBuilderContext.cs b/src/Hosting/Abstractions/src/WebHostBuilderContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..58e8d0798b0b37870b1f08c1b84283fd23ff13e2 --- /dev/null +++ b/src/Hosting/Abstractions/src/WebHostBuilderContext.cs @@ -0,0 +1,23 @@ +// 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.Extensions.Configuration; + +namespace Microsoft.AspNetCore.Hosting +{ + /// <summary> + /// Context containing the common services on the <see cref="IWebHost" />. Some properties may be null until set by the <see cref="IWebHost" />. + /// </summary> + public class WebHostBuilderContext + { + /// <summary> + /// The <see cref="IHostingEnvironment" /> initialized by the <see cref="IWebHost" />. + /// </summary> + public IHostingEnvironment HostingEnvironment { get; set; } + + /// <summary> + /// The <see cref="IConfiguration" /> containing the merged configuration of the application and the <see cref="IWebHost" />. + /// </summary> + public IConfiguration Configuration { get; set; } + } +} diff --git a/src/Hosting/Abstractions/src/WebHostDefaults.cs b/src/Hosting/Abstractions/src/WebHostDefaults.cs new file mode 100644 index 0000000000000000000000000000000000000000..4de391d0a28b9910963adc009bcaa833fabaa9b4 --- /dev/null +++ b/src/Hosting/Abstractions/src/WebHostDefaults.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Hosting +{ + public static class WebHostDefaults + { + public static readonly string ApplicationKey = "applicationName"; + public static readonly string StartupAssemblyKey = "startupAssembly"; + public static readonly string HostingStartupAssembliesKey = "hostingStartupAssemblies"; + public static readonly string HostingStartupExcludeAssembliesKey = "hostingStartupExcludeAssemblies"; + + public static readonly string DetailedErrorsKey = "detailedErrors"; + public static readonly string EnvironmentKey = "environment"; + public static readonly string WebRootKey = "webroot"; + public static readonly string CaptureStartupErrorsKey = "captureStartupErrors"; + public static readonly string ServerUrlsKey = "urls"; + public static readonly string ContentRootKey = "contentRoot"; + public static readonly string PreferHostingUrlsKey = "preferHostingUrls"; + public static readonly string PreventHostingStartupKey = "preventHostingStartup"; + public static readonly string SuppressStatusMessagesKey = "suppressStatusMessages"; + + public static readonly string ShutdownTimeoutKey = "shutdownTimeoutSeconds"; + } +} diff --git a/src/Hosting/Abstractions/src/baseline.netcore.json b/src/Hosting/Abstractions/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..7536bf12332e960616fd6ca8f4a9884ff3ec4964 --- /dev/null +++ b/src/Hosting/Abstractions/src/baseline.netcore.json @@ -0,0 +1,947 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Hosting.Abstractions, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Hosting.EnvironmentName", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Development", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Staging", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Production", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.HostingAbstractionsWebHostBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseConfiguration", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "configuration", + "Type": "Microsoft.Extensions.Configuration.IConfiguration" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CaptureStartupErrors", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "captureStartupErrors", + "Type": "System.Boolean" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseStartup", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "startupAssemblyName", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseServer", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "server", + "Type": "Microsoft.AspNetCore.Hosting.Server.IServer" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseEnvironment", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "environment", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseContentRoot", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "contentRoot", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseWebRoot", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "webRoot", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseUrls", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "urls", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "PreferHostingUrls", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "preferHostingUrls", + "Type": "System.Boolean" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseShutdownTimeout", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "timeout", + "Type": "System.TimeSpan" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Start", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "urls", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHost", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.HostingEnvironmentExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "IsDevelopment", + "Parameters": [ + { + "Name": "hostingEnvironment", + "Type": "Microsoft.AspNetCore.Hosting.IHostingEnvironment" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsStaging", + "Parameters": [ + { + "Name": "hostingEnvironment", + "Type": "Microsoft.AspNetCore.Hosting.IHostingEnvironment" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsProduction", + "Parameters": [ + { + "Name": "hostingEnvironment", + "Type": "Microsoft.AspNetCore.Hosting.IHostingEnvironment" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsEnvironment", + "Parameters": [ + { + "Name": "hostingEnvironment", + "Type": "Microsoft.AspNetCore.Hosting.IHostingEnvironment" + }, + { + "Name": "environmentName", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.HostingStartupAttribute", + "Visibility": "Public", + "Kind": "Class", + "Sealed": true, + "BaseType": "System.Attribute", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HostingStartupType", + "Parameters": [], + "ReturnType": "System.Type", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "hostingStartupType", + "Type": "System.Type" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.IApplicationLifetime", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ApplicationStarted", + "Parameters": [], + "ReturnType": "System.Threading.CancellationToken", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ApplicationStopping", + "Parameters": [], + "ReturnType": "System.Threading.CancellationToken", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ApplicationStopped", + "Parameters": [], + "ReturnType": "System.Threading.CancellationToken", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StopApplication", + "Parameters": [], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.IHostingEnvironment", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_EnvironmentName", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_EnvironmentName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ApplicationName", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ApplicationName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_WebRootPath", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_WebRootPath", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_WebRootFileProvider", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.FileProviders.IFileProvider", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_WebRootFileProvider", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.FileProviders.IFileProvider" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentRootPath", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentRootPath", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentRootFileProvider", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.FileProviders.IFileProvider", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentRootFileProvider", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.FileProviders.IFileProvider" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.IHostingStartup", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Configure", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.IStartup", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "ConfigureServices", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "System.IServiceProvider", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Configure", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.IStartupFilter", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Configure", + "Parameters": [ + { + "Name": "next", + "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>" + } + ], + "ReturnType": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.IWebHost", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "System.IDisposable" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ServerFeatures", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Services", + "Parameters": [], + "ReturnType": "System.IServiceProvider", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Start", + "Parameters": [], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StartAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StopAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Build", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHost", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ConfigureAppConfiguration", + "Parameters": [ + { + "Name": "configureDelegate", + "Type": "System.Action<Microsoft.AspNetCore.Hosting.WebHostBuilderContext, Microsoft.Extensions.Configuration.IConfigurationBuilder>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ConfigureServices", + "Parameters": [ + { + "Name": "configureServices", + "Type": "System.Action<Microsoft.Extensions.DependencyInjection.IServiceCollection>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ConfigureServices", + "Parameters": [ + { + "Name": "configureServices", + "Type": "System.Action<Microsoft.AspNetCore.Hosting.WebHostBuilderContext, Microsoft.Extensions.DependencyInjection.IServiceCollection>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetSetting", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseSetting", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.WebHostBuilderContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HostingEnvironment", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Hosting.IHostingEnvironment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HostingEnvironment", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Hosting.IHostingEnvironment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Configuration", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Configuration.IConfiguration", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Configuration", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Configuration.IConfiguration" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.WebHostDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "ApplicationKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "StartupAssemblyKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "HostingStartupAssembliesKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "DetailedErrorsKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "EnvironmentKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "WebRootKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "CaptureStartupErrorsKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "ServerUrlsKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "ContentRootKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "PreferHostingUrlsKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "PreventHostingStartupKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "ShutdownTimeoutKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Builder/ApplicationBuilderFactory.cs b/src/Hosting/Hosting/src/Builder/ApplicationBuilderFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..e188c0b7fd25c7df2987ec6d80b5c7ad15873373 --- /dev/null +++ b/src/Hosting/Hosting/src/Builder/ApplicationBuilderFactory.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Hosting.Builder +{ + public class ApplicationBuilderFactory : IApplicationBuilderFactory + { + private readonly IServiceProvider _serviceProvider; + + public ApplicationBuilderFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public IApplicationBuilder CreateBuilder(IFeatureCollection serverFeatures) + { + return new ApplicationBuilder(_serviceProvider, serverFeatures); + } + } +} diff --git a/src/Hosting/Hosting/src/Builder/IApplicationBuilderFactory.cs b/src/Hosting/Hosting/src/Builder/IApplicationBuilderFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..d44398fb69d89fefe98c1e20716d0cbb2343037b --- /dev/null +++ b/src/Hosting/Hosting/src/Builder/IApplicationBuilderFactory.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Hosting.Builder +{ + public interface IApplicationBuilderFactory + { + IApplicationBuilder CreateBuilder(IFeatureCollection serverFeatures); + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Internal/ApplicationLifetime.cs b/src/Hosting/Hosting/src/Internal/ApplicationLifetime.cs new file mode 100644 index 0000000000000000000000000000000000000000..958f8b5dcc1d339ca80d998d2d99d85f3f83c6fc --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/ApplicationLifetime.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + /// <summary> + /// Allows consumers to perform cleanup during a graceful shutdown. + /// </summary> + public class ApplicationLifetime : IApplicationLifetime, Extensions.Hosting.IApplicationLifetime + { + private readonly CancellationTokenSource _startedSource = new CancellationTokenSource(); + private readonly CancellationTokenSource _stoppingSource = new CancellationTokenSource(); + private readonly CancellationTokenSource _stoppedSource = new CancellationTokenSource(); + private readonly ILogger<ApplicationLifetime> _logger; + + public ApplicationLifetime(ILogger<ApplicationLifetime> logger) + { + _logger = logger; + } + + /// <summary> + /// Triggered when the application host has fully started and is about to wait + /// for a graceful shutdown. + /// </summary> + public CancellationToken ApplicationStarted => _startedSource.Token; + + /// <summary> + /// Triggered when the application host is performing a graceful shutdown. + /// Request may still be in flight. Shutdown will block until this event completes. + /// </summary> + public CancellationToken ApplicationStopping => _stoppingSource.Token; + + /// <summary> + /// Triggered when the application host is performing a graceful shutdown. + /// All requests should be complete at this point. Shutdown will block + /// until this event completes. + /// </summary> + public CancellationToken ApplicationStopped => _stoppedSource.Token; + + /// <summary> + /// Signals the ApplicationStopping event and blocks until it completes. + /// </summary> + public void StopApplication() + { + // Lock on CTS to synchronize multiple calls to StopApplication. This guarantees that the first call + // to StopApplication and its callbacks run to completion before subsequent calls to StopApplication, + // which will no-op since the first call already requested cancellation, get a chance to execute. + lock (_stoppingSource) + { + try + { + ExecuteHandlers(_stoppingSource); + } + catch (Exception ex) + { + _logger.ApplicationError(LoggerEventIds.ApplicationStoppingException, + "An error occurred stopping the application", + ex); + } + } + } + + /// <summary> + /// Signals the ApplicationStarted event and blocks until it completes. + /// </summary> + public void NotifyStarted() + { + try + { + ExecuteHandlers(_startedSource); + } + catch (Exception ex) + { + _logger.ApplicationError(LoggerEventIds.ApplicationStartupException, + "An error occurred starting the application", + ex); + } + } + + /// <summary> + /// Signals the ApplicationStopped event and blocks until it completes. + /// </summary> + public void NotifyStopped() + { + try + { + ExecuteHandlers(_stoppedSource); + } + catch (Exception ex) + { + _logger.ApplicationError(LoggerEventIds.ApplicationStoppedException, + "An error occurred stopping the application", + ex); + } + } + + private void ExecuteHandlers(CancellationTokenSource cancel) + { + // Noop if this is already cancelled + if (cancel.IsCancellationRequested) + { + return; + } + + // Run the cancellation token callbacks + cancel.Cancel(throwOnFirstException: false); + } + } +} diff --git a/src/Hosting/Hosting/src/Internal/AutoRequestServicesStartupFilter.cs b/src/Hosting/Hosting/src/Internal/AutoRequestServicesStartupFilter.cs new file mode 100644 index 0000000000000000000000000000000000000000..b75958fa52cf54074adcd7efbbb0ae7bafb5b0f8 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/AutoRequestServicesStartupFilter.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public class AutoRequestServicesStartupFilter : IStartupFilter + { + public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) + { + return builder => + { + builder.UseMiddleware<RequestServicesContainerMiddleware>(); + next(builder); + }; + } + } +} diff --git a/src/Hosting/Hosting/src/Internal/ConfigureBuilder.cs b/src/Hosting/Hosting/src/Internal/ConfigureBuilder.cs new file mode 100644 index 0000000000000000000000000000000000000000..37b715c5b04e1e534c5ad8561136932130af39f3 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/ConfigureBuilder.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public class ConfigureBuilder + { + public ConfigureBuilder(MethodInfo configure) + { + MethodInfo = configure; + } + + public MethodInfo MethodInfo { get; } + + public Action<IApplicationBuilder> Build(object instance) => builder => Invoke(instance, builder); + + private void Invoke(object instance, IApplicationBuilder builder) + { + // Create a scope for Configure, this allows creating scoped dependencies + // without the hassle of manually creating a scope. + using (var scope = builder.ApplicationServices.CreateScope()) + { + var serviceProvider = scope.ServiceProvider; + var parameterInfos = MethodInfo.GetParameters(); + var parameters = new object[parameterInfos.Length]; + for (var index = 0; index < parameterInfos.Length; index++) + { + var parameterInfo = parameterInfos[index]; + if (parameterInfo.ParameterType == typeof(IApplicationBuilder)) + { + parameters[index] = builder; + } + else + { + try + { + parameters[index] = serviceProvider.GetRequiredService(parameterInfo.ParameterType); + } + catch (Exception ex) + { + throw new Exception(string.Format( + "Could not resolve a service of type '{0}' for the parameter '{1}' of method '{2}' on type '{3}'.", + parameterInfo.ParameterType.FullName, + parameterInfo.Name, + MethodInfo.Name, + MethodInfo.DeclaringType.FullName), ex); + } + } + } + MethodInfo.Invoke(instance, parameters); + } + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Internal/ConfigureContainerBuilder.cs b/src/Hosting/Hosting/src/Internal/ConfigureContainerBuilder.cs new file mode 100644 index 0000000000000000000000000000000000000000..ed8d0fd06e842b4e6f7c914c2add13df20fdf62c --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/ConfigureContainerBuilder.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public class ConfigureContainerBuilder + { + public ConfigureContainerBuilder(MethodInfo configureContainerMethod) + { + MethodInfo = configureContainerMethod; + } + + public MethodInfo MethodInfo { get; } + + public Func<Action<object>, Action<object>> ConfigureContainerFilters { get; set; } + + public Action<object> Build(object instance) => container => Invoke(instance, container); + + public Type GetContainerType() + { + var parameters = MethodInfo.GetParameters(); + if (parameters.Length != 1) + { + // REVIEW: This might be a breaking change + throw new InvalidOperationException($"The {MethodInfo.Name} method must take only one parameter."); + } + return parameters[0].ParameterType; + } + + private void Invoke(object instance, object container) + { + ConfigureContainerFilters(StartupConfigureContainer)(container); + + void StartupConfigureContainer(object containerBuilder) => InvokeCore(instance, containerBuilder); + } + + private void InvokeCore(object instance, object container) + { + if (MethodInfo == null) + { + return; + } + + var arguments = new object[1] { container }; + + MethodInfo.Invoke(instance, arguments); + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Internal/ConfigureServicesBuilder.cs b/src/Hosting/Hosting/src/Internal/ConfigureServicesBuilder.cs new file mode 100644 index 0000000000000000000000000000000000000000..4206d0d62a0bac431acb2e5df3766cfd2c33cd21 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/ConfigureServicesBuilder.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public class ConfigureServicesBuilder + { + public ConfigureServicesBuilder(MethodInfo configureServices) + { + MethodInfo = configureServices; + } + + public MethodInfo MethodInfo { get; } + + public Func<Func<IServiceCollection, IServiceProvider>, Func<IServiceCollection, IServiceProvider>> StartupServiceFilters { get; set; } + + public Func<IServiceCollection, IServiceProvider> Build(object instance) => services => Invoke(instance, services); + + private IServiceProvider Invoke(object instance, IServiceCollection services) + { + return StartupServiceFilters(Startup)(services); + + IServiceProvider Startup(IServiceCollection serviceCollection) => InvokeCore(instance, serviceCollection); + } + + private IServiceProvider InvokeCore(object instance, IServiceCollection services) + { + if (MethodInfo == null) + { + return null; + } + + // Only support IServiceCollection parameters + var parameters = MethodInfo.GetParameters(); + if (parameters.Length > 1 || + parameters.Any(p => p.ParameterType != typeof(IServiceCollection))) + { + throw new InvalidOperationException("The ConfigureServices method must either be parameterless or take only one parameter of type IServiceCollection."); + } + + var arguments = new object[MethodInfo.GetParameters().Length]; + + if (parameters.Length > 0) + { + arguments[0] = services; + } + + return MethodInfo.Invoke(instance, arguments) as IServiceProvider; + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Internal/HostedServiceExecutor.cs b/src/Hosting/Hosting/src/Internal/HostedServiceExecutor.cs new file mode 100644 index 0000000000000000000000000000000000000000..ee6fbcfad8aba1d06dd1af45f8b7013db4ddcd9e --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/HostedServiceExecutor.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public class HostedServiceExecutor + { + private readonly IEnumerable<IHostedService> _services; + private readonly ILogger<HostedServiceExecutor> _logger; + + public HostedServiceExecutor(ILogger<HostedServiceExecutor> logger, IEnumerable<IHostedService> services) + { + _logger = logger; + _services = services; + } + + public async Task StartAsync(CancellationToken token) + { + try + { + await ExecuteAsync(service => service.StartAsync(token)); + } + catch (Exception ex) + { + _logger.ApplicationError(LoggerEventIds.HostedServiceStartException, "An error occurred starting the application", ex); + } + } + + public async Task StopAsync(CancellationToken token) + { + try + { + await ExecuteAsync(service => service.StopAsync(token)); + } + catch (Exception ex) + { + _logger.ApplicationError(LoggerEventIds.HostedServiceStopException, "An error occurred stopping the application", ex); + } + } + + private async Task ExecuteAsync(Func<IHostedService, Task> callback) + { + List<Exception> exceptions = null; + + foreach (var service in _services) + { + try + { + await callback(service); + } + catch (Exception ex) + { + if (exceptions == null) + { + exceptions = new List<Exception>(); + } + + exceptions.Add(ex); + } + } + + // Throw an aggregate exception if there were any exceptions + if (exceptions != null) + { + throw new AggregateException(exceptions); + } + } + } +} diff --git a/src/Hosting/Hosting/src/Internal/HostingApplication.cs b/src/Hosting/Hosting/src/Internal/HostingApplication.cs new file mode 100644 index 0000000000000000000000000000000000000000..cd3e2d9fb21b0d1e41b2caab25ed741536b3c82a --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/HostingApplication.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public class HostingApplication : IHttpApplication<HostingApplication.Context> + { + private readonly RequestDelegate _application; + private readonly IHttpContextFactory _httpContextFactory; + private HostingApplicationDiagnostics _diagnostics; + + public HostingApplication( + RequestDelegate application, + ILogger logger, + DiagnosticListener diagnosticSource, + IHttpContextFactory httpContextFactory) + { + _application = application; + _diagnostics = new HostingApplicationDiagnostics(logger, diagnosticSource); + _httpContextFactory = httpContextFactory; + } + + // Set up the request + public Context CreateContext(IFeatureCollection contextFeatures) + { + var context = new Context(); + var httpContext = _httpContextFactory.Create(contextFeatures); + + _diagnostics.BeginRequest(httpContext, ref context); + + context.HttpContext = httpContext; + return context; + } + + // Execute the request + public Task ProcessRequestAsync(Context context) + { + return _application(context.HttpContext); + } + + // Clean up the request + public void DisposeContext(Context context, Exception exception) + { + var httpContext = context.HttpContext; + _diagnostics.RequestEnd(httpContext, exception, context); + _httpContextFactory.Dispose(httpContext); + _diagnostics.ContextDisposed(context); + } + + public struct Context + { + public HttpContext HttpContext { get; set; } + public IDisposable Scope { get; set; } + public long StartTimestamp { get; set; } + public bool EventLogEnabled { get; set; } + public Activity Activity { get; set; } + } + } +} diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs new file mode 100644 index 0000000000000000000000000000000000000000..d485b1a060ce9b91bf877fea19841271d0601525 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -0,0 +1,283 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + internal class HostingApplicationDiagnostics + { + private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + + private const string ActivityName = "Microsoft.AspNetCore.Hosting.HttpRequestIn"; + private const string ActivityStartKey = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start"; + + private const string DeprecatedDiagnosticsBeginRequestKey = "Microsoft.AspNetCore.Hosting.BeginRequest"; + private const string DeprecatedDiagnosticsEndRequestKey = "Microsoft.AspNetCore.Hosting.EndRequest"; + private const string DiagnosticsUnhandledExceptionKey = "Microsoft.AspNetCore.Hosting.UnhandledException"; + + private const string RequestIdHeaderName = "Request-Id"; + private const string CorrelationContextHeaderName = "Correlation-Context"; + + private readonly DiagnosticListener _diagnosticListener; + private readonly ILogger _logger; + + public HostingApplicationDiagnostics(ILogger logger, DiagnosticListener diagnosticListener) + { + _logger = logger; + _diagnosticListener = diagnosticListener; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void BeginRequest(HttpContext httpContext, ref HostingApplication.Context context) + { + long startTimestamp = 0; + + if (HostingEventSource.Log.IsEnabled()) + { + context.EventLogEnabled = true; + // To keep the hot path short we defer logging in this function to non-inlines + RecordRequestStartEventLog(httpContext); + } + + var diagnosticListenerEnabled = _diagnosticListener.IsEnabled(); + var loggingEnabled = _logger.IsEnabled(LogLevel.Critical); + + // If logging is enabled or the diagnostic listener is enabled, try to get the correlation + // id from the header + StringValues correlationId; + if (diagnosticListenerEnabled || loggingEnabled) + { + httpContext.Request.Headers.TryGetValue(RequestIdHeaderName, out correlationId); + } + + if (diagnosticListenerEnabled) + { + if (_diagnosticListener.IsEnabled(ActivityName, httpContext)) + { + context.Activity = StartActivity(httpContext, correlationId); + } + if (_diagnosticListener.IsEnabled(DeprecatedDiagnosticsBeginRequestKey)) + { + startTimestamp = Stopwatch.GetTimestamp(); + RecordBeginRequestDiagnostics(httpContext, startTimestamp); + } + } + + // To avoid allocation, return a null scope if the logger is not on at least to some degree. + if (loggingEnabled) + { + // Scope may be relevant for a different level of logging, so we always create it + // see: https://github.com/aspnet/Hosting/pull/944 + // Scope can be null if logging is not on. + context.Scope = _logger.RequestScope(httpContext, correlationId); + + if (_logger.IsEnabled(LogLevel.Information)) + { + if (startTimestamp == 0) + { + startTimestamp = Stopwatch.GetTimestamp(); + } + + // Non-inline + LogRequestStarting(httpContext); + } + } + + context.StartTimestamp = startTimestamp; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RequestEnd(HttpContext httpContext, Exception exception, HostingApplication.Context context) + { + // Local cache items resolved multiple items, in order of use so they are primed in cpu pipeline when used + var startTimestamp = context.StartTimestamp; + long currentTimestamp = 0; + + // If startTimestamp was 0, then Information logging wasn't enabled at for this request (and calcuated time will be wildly wrong) + // Is used as proxy to reduce calls to virtual: _logger.IsEnabled(LogLevel.Information) + if (startTimestamp != 0) + { + currentTimestamp = Stopwatch.GetTimestamp(); + // Non-inline + LogRequestFinished(httpContext, startTimestamp, currentTimestamp); + } + + if (_diagnosticListener.IsEnabled()) + { + if (currentTimestamp == 0) + { + currentTimestamp = Stopwatch.GetTimestamp(); + } + + if (exception == null) + { + // No exception was thrown, request was sucessful + if (_diagnosticListener.IsEnabled(DeprecatedDiagnosticsEndRequestKey)) + { + // Diagnostics is enabled for EndRequest, but it may not be for BeginRequest + // so call GetTimestamp if currentTimestamp is zero (from above) + RecordEndRequestDiagnostics(httpContext, currentTimestamp); + } + } + else + { + // Exception was thrown from request + if (_diagnosticListener.IsEnabled(DiagnosticsUnhandledExceptionKey)) + { + // Diagnostics is enabled for UnhandledException, but it may not be for BeginRequest + // so call GetTimestamp if currentTimestamp is zero (from above) + RecordUnhandledExceptionDiagnostics(httpContext, currentTimestamp, exception); + } + + } + + var activity = context.Activity; + // Always stop activity if it was started + if (activity != null) + { + StopActivity(httpContext, activity); + } + } + + if (context.EventLogEnabled && exception != null) + { + // Non-inline + HostingEventSource.Log.UnhandledException(); + } + + // Logging Scope is finshed with + context.Scope?.Dispose(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ContextDisposed(HostingApplication.Context context) + { + if (context.EventLogEnabled) + { + // Non-inline + HostingEventSource.Log.RequestStop(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void LogRequestStarting(HttpContext httpContext) + { + // IsEnabled is checked in the caller, so if we are here just log + _logger.Log( + logLevel: LogLevel.Information, + eventId: LoggerEventIds.RequestStarting, + state: new HostingRequestStartingLog(httpContext), + exception: null, + formatter: HostingRequestStartingLog.Callback); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void LogRequestFinished(HttpContext httpContext, long startTimestamp, long currentTimestamp) + { + // IsEnabled isn't checked in the caller, startTimestamp > 0 is used as a fast proxy check + // but that may be because diagnostics are enabled, which also uses startTimestamp, so check here + if (_logger.IsEnabled(LogLevel.Information)) + { + var elapsed = new TimeSpan((long)(TimestampToTicks * (currentTimestamp - startTimestamp))); + + _logger.Log( + logLevel: LogLevel.Information, + eventId: LoggerEventIds.RequestFinished, + state: new HostingRequestFinishedLog(httpContext, elapsed), + exception: null, + formatter: HostingRequestFinishedLog.Callback); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void RecordBeginRequestDiagnostics(HttpContext httpContext, long startTimestamp) + { + _diagnosticListener.Write( + DeprecatedDiagnosticsBeginRequestKey, + new + { + httpContext = httpContext, + timestamp = startTimestamp + }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void RecordEndRequestDiagnostics(HttpContext httpContext, long currentTimestamp) + { + _diagnosticListener.Write( + DeprecatedDiagnosticsEndRequestKey, + new + { + httpContext = httpContext, + timestamp = currentTimestamp + }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void RecordUnhandledExceptionDiagnostics(HttpContext httpContext, long currentTimestamp, Exception exception) + { + _diagnosticListener.Write( + DiagnosticsUnhandledExceptionKey, + new + { + httpContext = httpContext, + timestamp = currentTimestamp, + exception = exception + }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void RecordRequestStartEventLog(HttpContext httpContext) + { + HostingEventSource.Log.RequestStart(httpContext.Request.Method, httpContext.Request.Path); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private Activity StartActivity(HttpContext httpContext, StringValues requestId) + { + var activity = new Activity(ActivityName); + if (!StringValues.IsNullOrEmpty(requestId)) + { + activity.SetParentId(requestId); + + // We expect baggage to be empty by default + // Only very advanced users will be using it in near future, we encourage them to keep baggage small (few items) + string[] baggage = httpContext.Request.Headers.GetCommaSeparatedValues(CorrelationContextHeaderName); + if (baggage != StringValues.Empty) + { + foreach (var item in baggage) + { + if (NameValueHeaderValue.TryParse(item, out var baggageItem)) + { + activity.AddBaggage(baggageItem.Name.ToString(), baggageItem.Value.ToString()); + } + } + } + } + + if (_diagnosticListener.IsEnabled(ActivityStartKey)) + { + _diagnosticListener.StartActivity(activity, new { HttpContext = httpContext }); + } + else + { + activity.Start(); + } + + return activity; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void StopActivity(HttpContext httpContext, Activity activity) + { + _diagnosticListener.StopActivity(activity, new { HttpContext = httpContext }); + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Internal/HostingEnvironment.cs b/src/Hosting/Hosting/src/Internal/HostingEnvironment.cs new file mode 100644 index 0000000000000000000000000000000000000000..1f8d1887d7d41c371beb50205d0abd9b389ac33f --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/HostingEnvironment.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public class HostingEnvironment : IHostingEnvironment, Extensions.Hosting.IHostingEnvironment + { + public string EnvironmentName { get; set; } = Hosting.EnvironmentName.Production; + + public string ApplicationName { get; set; } + + public string WebRootPath { get; set; } + + public IFileProvider WebRootFileProvider { get; set; } + + public string ContentRootPath { get; set; } + + public IFileProvider ContentRootFileProvider { get; set; } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Internal/HostingEnvironmentExtensions.cs b/src/Hosting/Hosting/src/Internal/HostingEnvironmentExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..c12d0bcdd6285610809cf7210c116b6a7981ad55 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/HostingEnvironmentExtensions.cs @@ -0,0 +1,65 @@ +// 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 Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public static class HostingEnvironmentExtensions + { + public static void Initialize(this IHostingEnvironment hostingEnvironment, string contentRootPath, WebHostOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + if (string.IsNullOrEmpty(contentRootPath)) + { + throw new ArgumentException("A valid non-empty content root must be provided.", nameof(contentRootPath)); + } + if (!Directory.Exists(contentRootPath)) + { + throw new ArgumentException($"The content root '{contentRootPath}' does not exist.", nameof(contentRootPath)); + } + + hostingEnvironment.ApplicationName = options.ApplicationName; + hostingEnvironment.ContentRootPath = contentRootPath; + hostingEnvironment.ContentRootFileProvider = new PhysicalFileProvider(hostingEnvironment.ContentRootPath); + + var webRoot = options.WebRoot; + if (webRoot == null) + { + // Default to /wwwroot if it exists. + var wwwroot = Path.Combine(hostingEnvironment.ContentRootPath, "wwwroot"); + if (Directory.Exists(wwwroot)) + { + hostingEnvironment.WebRootPath = wwwroot; + } + } + else + { + hostingEnvironment.WebRootPath = Path.Combine(hostingEnvironment.ContentRootPath, webRoot); + } + + if (!string.IsNullOrEmpty(hostingEnvironment.WebRootPath)) + { + hostingEnvironment.WebRootPath = Path.GetFullPath(hostingEnvironment.WebRootPath); + if (!Directory.Exists(hostingEnvironment.WebRootPath)) + { + Directory.CreateDirectory(hostingEnvironment.WebRootPath); + } + hostingEnvironment.WebRootFileProvider = new PhysicalFileProvider(hostingEnvironment.WebRootPath); + } + else + { + hostingEnvironment.WebRootFileProvider = new NullFileProvider(); + } + + hostingEnvironment.EnvironmentName = + options.Environment ?? + hostingEnvironment.EnvironmentName; + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Internal/HostingEventSource.cs b/src/Hosting/Hosting/src/Internal/HostingEventSource.cs new file mode 100644 index 0000000000000000000000000000000000000000..199f8aae85c74a1a82f5c2e4abe00e853715e661 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/HostingEventSource.cs @@ -0,0 +1,55 @@ +// 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.Diagnostics.Tracing; +using System.Runtime.CompilerServices; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + [EventSource(Name = "Microsoft-AspNetCore-Hosting")] + public sealed class HostingEventSource : EventSource + { + public static readonly HostingEventSource Log = new HostingEventSource(); + + private HostingEventSource() { } + + // NOTE + // - The 'Start' and 'Stop' suffixes on the following event names have special meaning in EventSource. They + // enable creating 'activities'. + // For more information, take a look at the following blog post: + // https://blogs.msdn.microsoft.com/vancem/2015/09/14/exploring-eventsource-activity-correlation-and-causation-features/ + // - A stop event's event id must be next one after its start event. + + [Event(1, Level = EventLevel.Informational)] + public void HostStart() + { + WriteEvent(1); + } + + [Event(2, Level = EventLevel.Informational)] + public void HostStop() + { + WriteEvent(2); + } + + [Event(3, Level = EventLevel.Informational)] + public void RequestStart(string method, string path) + { + WriteEvent(3, method, path); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + [Event(4, Level = EventLevel.Informational)] + public void RequestStop() + { + WriteEvent(4); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + [Event(5, Level = EventLevel.Error)] + public void UnhandledException() + { + WriteEvent(5); + } + } +} diff --git a/src/Hosting/Hosting/src/Internal/HostingLoggerExtensions.cs b/src/Hosting/Hosting/src/Internal/HostingLoggerExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..a0579880a0a8d24de429498c25062e1b9c975a82 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/HostingLoggerExtensions.cs @@ -0,0 +1,166 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + internal static class HostingLoggerExtensions + { + public static IDisposable RequestScope(this ILogger logger, HttpContext httpContext, string correlationId) + { + return logger.BeginScope(new HostingLogScope(httpContext, correlationId)); + } + + public static void ApplicationError(this ILogger logger, Exception exception) + { + logger.ApplicationError( + eventId: LoggerEventIds.ApplicationStartupException, + message: "Application startup exception", + exception: exception); + } + + public static void HostingStartupAssemblyError(this ILogger logger, Exception exception) + { + logger.ApplicationError( + eventId: LoggerEventIds.HostingStartupAssemblyException, + message: "Hosting startup assembly exception", + exception: exception); + } + + public static void ApplicationError(this ILogger logger, EventId eventId, string message, Exception exception) + { + var reflectionTypeLoadException = exception as ReflectionTypeLoadException; + if (reflectionTypeLoadException != null) + { + foreach (var ex in reflectionTypeLoadException.LoaderExceptions) + { + message = message + Environment.NewLine + ex.Message; + } + } + + logger.LogCritical( + eventId: eventId, + message: message, + exception: exception); + } + + public static void Starting(this ILogger logger) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug( + eventId: LoggerEventIds.Starting, + message: "Hosting starting"); + } + } + + public static void Started(this ILogger logger) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug( + eventId: LoggerEventIds.Started, + message: "Hosting started"); + } + } + + public static void Shutdown(this ILogger logger) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug( + eventId: LoggerEventIds.Shutdown, + message: "Hosting shutdown"); + } + } + + public static void ServerShutdownException(this ILogger logger, Exception ex) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug( + eventId: LoggerEventIds.ServerShutdownException, + exception: ex, + message: "Server shutdown exception"); + } + } + + private class HostingLogScope : IReadOnlyList<KeyValuePair<string, object>> + { + private readonly HttpContext _httpContext; + private readonly string _correlationId; + + private string _cachedToString; + + public int Count + { + get + { + return 3; + } + } + + public KeyValuePair<string, object> this[int index] + { + get + { + if (index == 0) + { + return new KeyValuePair<string, object>("RequestId", _httpContext.TraceIdentifier); + } + else if (index == 1) + { + return new KeyValuePair<string, object>("RequestPath", _httpContext.Request.Path.ToString()); + } + else if (index == 2) + { + return new KeyValuePair<string, object>("CorrelationId", _correlationId); + } + + throw new ArgumentOutOfRangeException(nameof(index)); + } + } + + public HostingLogScope(HttpContext httpContext, string correlationId) + { + _httpContext = httpContext; + _correlationId = correlationId; + } + + public override string ToString() + { + if (_cachedToString == null) + { + _cachedToString = string.Format( + CultureInfo.InvariantCulture, + "RequestId:{0} RequestPath:{1}", + _httpContext.TraceIdentifier, + _httpContext.Request.Path); + } + + return _cachedToString; + } + + public IEnumerator<KeyValuePair<string, object>> GetEnumerator() + { + for (int i = 0; i < Count; ++i) + { + yield return this[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + } +} + diff --git a/src/Hosting/Hosting/src/Internal/HostingRequestFinishedLog.cs b/src/Hosting/Hosting/src/Internal/HostingRequestFinishedLog.cs new file mode 100644 index 0000000000000000000000000000000000000000..ab440481bae7d4f7d0800cdb4cf1547c8a097f50 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/HostingRequestFinishedLog.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + internal class HostingRequestFinishedLog : IReadOnlyList<KeyValuePair<string, object>> + { + internal static readonly Func<object, Exception, string> Callback = (state, exception) => ((HostingRequestFinishedLog)state).ToString(); + + private readonly HttpContext _httpContext; + private readonly TimeSpan _elapsed; + + private string _cachedToString; + + public int Count => 3; + + public KeyValuePair<string, object> this[int index] + { + get + { + switch (index) + { + case 0: + return new KeyValuePair<string, object>("ElapsedMilliseconds", _elapsed.TotalMilliseconds); + case 1: + return new KeyValuePair<string, object>("StatusCode", _httpContext.Response.StatusCode); + case 2: + return new KeyValuePair<string, object>("ContentType", _httpContext.Response.ContentType); + default: + throw new IndexOutOfRangeException(nameof(index)); + } + } + } + + public HostingRequestFinishedLog(HttpContext httpContext, TimeSpan elapsed) + { + _httpContext = httpContext; + _elapsed = elapsed; + } + + public override string ToString() + { + if (_cachedToString == null) + { + _cachedToString = string.Format( + CultureInfo.InvariantCulture, + "Request finished in {0}ms {1} {2}", + _elapsed.TotalMilliseconds, + _httpContext.Response.StatusCode, + _httpContext.Response.ContentType); + } + + return _cachedToString; + } + + public IEnumerator<KeyValuePair<string, object>> GetEnumerator() + { + for (var i = 0; i < Count; i++) + { + yield return this[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Hosting/Hosting/src/Internal/HostingRequestStartingLog.cs b/src/Hosting/Hosting/src/Internal/HostingRequestStartingLog.cs new file mode 100644 index 0000000000000000000000000000000000000000..7506028a3c67cc19b5246d462142186c9ec2fce7 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/HostingRequestStartingLog.cs @@ -0,0 +1,91 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + internal class HostingRequestStartingLog : IReadOnlyList<KeyValuePair<string, object>> + { + internal static readonly Func<object, Exception, string> Callback = (state, exception) => ((HostingRequestStartingLog)state).ToString(); + + private readonly HttpRequest _request; + + private string _cachedToString; + + public int Count => 9; + + public KeyValuePair<string, object> this[int index] + { + get + { + switch (index) + { + case 0: + return new KeyValuePair<string, object>("Protocol", _request.Protocol); + case 1: + return new KeyValuePair<string, object>("Method", _request.Method); + case 2: + return new KeyValuePair<string, object>("ContentType", _request.ContentType); + case 3: + return new KeyValuePair<string, object>("ContentLength", _request.ContentLength); + case 4: + return new KeyValuePair<string, object>("Scheme", _request.Scheme.ToString()); + case 5: + return new KeyValuePair<string, object>("Host", _request.Host.ToString()); + case 6: + return new KeyValuePair<string, object>("PathBase", _request.PathBase.ToString()); + case 7: + return new KeyValuePair<string, object>("Path", _request.Path.ToString()); + case 8: + return new KeyValuePair<string, object>("QueryString", _request.QueryString.ToString()); + default: + throw new IndexOutOfRangeException(nameof(index)); + } + } + } + + public HostingRequestStartingLog(HttpContext httpContext) + { + _request = httpContext.Request; + } + + public override string ToString() + { + if (_cachedToString == null) + { + _cachedToString = string.Format( + CultureInfo.InvariantCulture, + "Request starting {0} {1} {2}://{3}{4}{5}{6} {7} {8}", + _request.Protocol, + _request.Method, + _request.Scheme, + _request.Host.Value, + _request.PathBase.Value, + _request.Path.Value, + _request.QueryString.Value, + _request.ContentType, + _request.ContentLength); + } + + return _cachedToString; + } + + public IEnumerator<KeyValuePair<string, object>> GetEnumerator() + { + for (var i = 0; i < Count; i++) + { + yield return this[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Hosting/Hosting/src/Internal/LoggerEventIds.cs b/src/Hosting/Hosting/src/Internal/LoggerEventIds.cs new file mode 100644 index 0000000000000000000000000000000000000000..f7d8f61933a8fca96c078dd4403af35033d3bbbd --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/LoggerEventIds.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + internal static class LoggerEventIds + { + public const int RequestStarting = 1; + public const int RequestFinished = 2; + public const int Starting = 3; + public const int Started = 4; + public const int Shutdown = 5; + public const int ApplicationStartupException = 6; + public const int ApplicationStoppingException = 7; + public const int ApplicationStoppedException = 8; + public const int HostedServiceStartException = 9; + public const int HostedServiceStopException = 10; + public const int HostingStartupAssemblyException = 11; + public const int ServerShutdownException = 12; + } +} diff --git a/src/Hosting/Hosting/src/Internal/RequestServicesContainerMiddleware.cs b/src/Hosting/Hosting/src/Internal/RequestServicesContainerMiddleware.cs new file mode 100644 index 0000000000000000000000000000000000000000..9fcb01aa12f631d8118feaaa1e21d812eb5a369c --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/RequestServicesContainerMiddleware.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public class RequestServicesContainerMiddleware + { + private readonly RequestDelegate _next; + private readonly IServiceScopeFactory _scopeFactory; + + public RequestServicesContainerMiddleware(RequestDelegate next, IServiceScopeFactory scopeFactory) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + if (scopeFactory == null) + { + throw new ArgumentNullException(nameof(scopeFactory)); + } + + _next = next; + _scopeFactory = scopeFactory; + } + + public Task Invoke(HttpContext httpContext) + { + Debug.Assert(httpContext != null); + + var features = httpContext.Features; + var servicesFeature = features.Get<IServiceProvidersFeature>(); + + // All done if RequestServices is set + if (servicesFeature?.RequestServices != null) + { + return _next.Invoke(httpContext); + } + + features.Set<IServiceProvidersFeature>(new RequestServicesFeature(httpContext, _scopeFactory)); + return _next.Invoke(httpContext); + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Internal/RequestServicesFeature.cs b/src/Hosting/Hosting/src/Internal/RequestServicesFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..a57df9bcbc493140ed35a142738f9c0a1f618cf6 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/RequestServicesFeature.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public class RequestServicesFeature : IServiceProvidersFeature, IDisposable + { + private readonly IServiceScopeFactory _scopeFactory; + private IServiceProvider _requestServices; + private IServiceScope _scope; + private bool _requestServicesSet; + private HttpContext _context; + + public RequestServicesFeature(HttpContext context, IServiceScopeFactory scopeFactory) + { + Debug.Assert(scopeFactory != null); + _context = context; + _scopeFactory = scopeFactory; + } + + public IServiceProvider RequestServices + { + get + { + if (!_requestServicesSet) + { + _context.Response.RegisterForDispose(this); + _scope = _scopeFactory.CreateScope(); + _requestServices = _scope.ServiceProvider; + _requestServicesSet = true; + } + return _requestServices; + } + + set + { + _requestServices = value; + _requestServicesSet = true; + } + } + + public void Dispose() + { + _scope?.Dispose(); + _scope = null; + _requestServices = null; + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Internal/ServiceCollectionExtensions.cs b/src/Hosting/Hosting/src/Internal/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..48b8758181e4022048acbe1410e814609ee2466b --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/ServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + internal static class ServiceCollectionExtensions + { + public static IServiceCollection Clone(this IServiceCollection serviceCollection) + { + IServiceCollection clone = new ServiceCollection(); + foreach (var service in serviceCollection) + { + clone.Add(service); + } + return clone; + } + } +} diff --git a/src/Hosting/Hosting/src/Internal/StartupLoader.cs b/src/Hosting/Hosting/src/Internal/StartupLoader.cs new file mode 100644 index 0000000000000000000000000000000000000000..d7211d39d9d66b349b397f12599f2884789a8f50 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/StartupLoader.cs @@ -0,0 +1,336 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public class StartupLoader + { + // Creates an <see cref="StartupMethods"/> instance with the actions to run for configuring the application services and the + // request pipeline of the application. + // When using convention based startup, the process for initializing the services is as follows: + // The host looks for a method with the signature <see cref="IServiceProvider"/> ConfigureServices(<see cref="IServiceCollection"/> services). + // If it can't find one, it looks for a method with the signature <see cref="void"/> ConfigureServices(<see cref="IServiceCollection"/> services). + // When the configure services method is void returning, the host builds a services configuration function that runs all the <see cref="IStartupConfigureServicesFilter"/> + // instances registered on the host, along with the ConfigureServices method following a decorator pattern. + // Additionally to the ConfigureServices method, the Startup class can define a <see cref="void"/> ConfigureContainer<TContainerBuilder>(TContainerBuilder builder) + // method that further configures services into the container. If the ConfigureContainer method is defined, the services configuration function + // creates a TContainerBuilder <see cref="IServiceProviderFactory{TContainerBuilder}"/> and runs all the <see cref="IStartupConfigureContainerFilter{TContainerBuilder}"/> + // instances registered on the host, along with the ConfigureContainer method following a decorator pattern. + // For example: + // StartupFilter1 + // StartupFilter2 + // ConfigureServices + // StartupFilter2 + // StartupFilter1 + // ConfigureContainerFilter1 + // ConfigureContainerFilter2 + // ConfigureContainer + // ConfigureContainerFilter2 + // ConfigureContainerFilter1 + // + // If the Startup class ConfigureServices returns an <see cref="IServiceProvider"/> and there is at least an <see cref="IStartupConfigureServicesFilter"/> registered we + // throw as the filters can't be applied. + public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider, Type startupType, string environmentName) + { + var configureMethod = FindConfigureDelegate(startupType, environmentName); + + var servicesMethod = FindConfigureServicesDelegate(startupType, environmentName); + var configureContainerMethod = FindConfigureContainerDelegate(startupType, environmentName); + + object instance = null; + if (!configureMethod.MethodInfo.IsStatic || (servicesMethod != null && !servicesMethod.MethodInfo.IsStatic)) + { + instance = ActivatorUtilities.GetServiceOrCreateInstance(hostingServiceProvider, startupType); + } + + // The type of the TContainerBuilder. If there is no ConfigureContainer method we can just use object as it's not + // going to be used for anything. + var type = configureContainerMethod.MethodInfo != null ? configureContainerMethod.GetContainerType() : typeof(object); + + var builder = (ConfigureServicesDelegateBuilder) Activator.CreateInstance( + typeof(ConfigureServicesDelegateBuilder<>).MakeGenericType(type), + hostingServiceProvider, + servicesMethod, + configureContainerMethod, + instance); + + return new StartupMethods(instance, configureMethod.Build(instance), builder.Build()); + } + + private abstract class ConfigureServicesDelegateBuilder + { + public abstract Func<IServiceCollection, IServiceProvider> Build(); + } + + private class ConfigureServicesDelegateBuilder<TContainerBuilder> : ConfigureServicesDelegateBuilder + { + public ConfigureServicesDelegateBuilder( + IServiceProvider hostingServiceProvider, + ConfigureServicesBuilder configureServicesBuilder, + ConfigureContainerBuilder configureContainerBuilder, + object instance) + { + HostingServiceProvider = hostingServiceProvider; + ConfigureServicesBuilder = configureServicesBuilder; + ConfigureContainerBuilder = configureContainerBuilder; + Instance = instance; + } + + public IServiceProvider HostingServiceProvider { get; } + public ConfigureServicesBuilder ConfigureServicesBuilder { get; } + public ConfigureContainerBuilder ConfigureContainerBuilder { get; } + public object Instance { get; } + + public override Func<IServiceCollection, IServiceProvider> Build() + { + ConfigureServicesBuilder.StartupServiceFilters = BuildStartupServicesFilterPipeline; + var configureServicesCallback = ConfigureServicesBuilder.Build(Instance); + + ConfigureContainerBuilder.ConfigureContainerFilters = ConfigureContainerPipeline; + var configureContainerCallback = ConfigureContainerBuilder.Build(Instance); + + return ConfigureServices(configureServicesCallback, configureContainerCallback); + + Action<object> ConfigureContainerPipeline(Action<object> action) + { + return Target; + + // The ConfigureContainer pipeline needs an Action<TContainerBuilder> as source, so we just adapt the + // signature with this function. + void Source(TContainerBuilder containerBuilder) => + action(containerBuilder); + + // The ConfigureContainerBuilder.ConfigureContainerFilters expects an Action<object> as value, but our pipeline + // produces an Action<TContainerBuilder> given a source, so we wrap it on an Action<object> that internally casts + // the object containerBuilder to TContainerBuilder to match the expected signature of our ConfigureContainer pipeline. + void Target(object containerBuilder) => + BuildStartupConfigureContainerFiltersPipeline(Source)((TContainerBuilder)containerBuilder); + } + } + + Func<IServiceCollection, IServiceProvider> ConfigureServices( + Func<IServiceCollection, IServiceProvider> configureServicesCallback, + Action<object> configureContainerCallback) + { + return ConfigureServicesWithContainerConfiguration; + + IServiceProvider ConfigureServicesWithContainerConfiguration(IServiceCollection services) + { + // Call ConfigureServices, if that returned an IServiceProvider, we're done + IServiceProvider applicationServiceProvider = configureServicesCallback.Invoke(services); + + if (applicationServiceProvider != null) + { + return applicationServiceProvider; + } + + // If there's a ConfigureContainer method + if (ConfigureContainerBuilder.MethodInfo != null) + { + var serviceProviderFactory = HostingServiceProvider.GetRequiredService<IServiceProviderFactory<TContainerBuilder>>(); + var builder = serviceProviderFactory.CreateBuilder(services); + configureContainerCallback(builder); + applicationServiceProvider = serviceProviderFactory.CreateServiceProvider(builder); + } + else + { + // Get the default factory + var serviceProviderFactory = HostingServiceProvider.GetRequiredService<IServiceProviderFactory<IServiceCollection>>(); + var builder = serviceProviderFactory.CreateBuilder(services); + applicationServiceProvider = serviceProviderFactory.CreateServiceProvider(builder); + } + + return applicationServiceProvider ?? services.BuildServiceProvider(); + } + } + + private Func<IServiceCollection, IServiceProvider> BuildStartupServicesFilterPipeline(Func<IServiceCollection, IServiceProvider> startup) + { + return RunPipeline; + + IServiceProvider RunPipeline(IServiceCollection services) + { + var filters = HostingServiceProvider.GetRequiredService<IEnumerable<IStartupConfigureServicesFilter>>().Reverse().ToArray(); + + // If there are no filters just run startup (makes IServiceProvider ConfigureServices(IServiceCollection services) work. + if (filters.Length == 0) + { + return startup(services); + } + + Action<IServiceCollection> pipeline = InvokeStartup; + for (int i = 0; i < filters.Length; i++) + { + pipeline = filters[i].ConfigureServices(pipeline); + } + + pipeline(services); + + // We return null so that the host here builds the container (same result as void ConfigureServices(IServiceCollection services); + return null; + + void InvokeStartup(IServiceCollection serviceCollection) + { + var result = startup(serviceCollection); + if (filters.Length > 0 && result != null) + { + // public IServiceProvider ConfigureServices(IServiceCollection serviceCollection) is not compatible with IStartupServicesFilter; + var message = $"A ConfigureServices method that returns an {nameof(IServiceProvider)} is " + + $"not compatible with the use of one or more {nameof(IStartupConfigureServicesFilter)}. " + + $"Use a void returning ConfigureServices method instead or a ConfigureContainer method."; + throw new InvalidOperationException(message); + }; + } + } + } + + private Action<TContainerBuilder> BuildStartupConfigureContainerFiltersPipeline(Action<TContainerBuilder> configureContainer) + { + return RunPipeline; + + void RunPipeline(TContainerBuilder containerBuilder) + { + var filters = HostingServiceProvider + .GetRequiredService<IEnumerable<IStartupConfigureContainerFilter<TContainerBuilder>>>() + .Reverse() + .ToArray(); + + Action<TContainerBuilder> pipeline = InvokeConfigureContainer; + for (int i = 0; i < filters.Length; i++) + { + pipeline = filters[i].ConfigureContainer(pipeline); + } + + pipeline(containerBuilder); + + void InvokeConfigureContainer(TContainerBuilder builder) => configureContainer(builder); + } + } + } + + public static Type FindStartupType(string startupAssemblyName, string environmentName) + { + if (string.IsNullOrEmpty(startupAssemblyName)) + { + throw new ArgumentException( + string.Format("A startup method, startup type or startup assembly is required. If specifying an assembly, '{0}' cannot be null or empty.", + nameof(startupAssemblyName)), + nameof(startupAssemblyName)); + } + + var assembly = Assembly.Load(new AssemblyName(startupAssemblyName)); + if (assembly == null) + { + throw new InvalidOperationException(String.Format("The assembly '{0}' failed to load.", startupAssemblyName)); + } + + var startupNameWithEnv = "Startup" + environmentName; + var startupNameWithoutEnv = "Startup"; + + // Check the most likely places first + var type = + assembly.GetType(startupNameWithEnv) ?? + assembly.GetType(startupAssemblyName + "." + startupNameWithEnv) ?? + assembly.GetType(startupNameWithoutEnv) ?? + assembly.GetType(startupAssemblyName + "." + startupNameWithoutEnv); + + if (type == null) + { + // Full scan + var definedTypes = assembly.DefinedTypes.ToList(); + + var startupType1 = definedTypes.Where(info => info.Name.Equals(startupNameWithEnv, StringComparison.OrdinalIgnoreCase)); + var startupType2 = definedTypes.Where(info => info.Name.Equals(startupNameWithoutEnv, StringComparison.OrdinalIgnoreCase)); + + var typeInfo = startupType1.Concat(startupType2).FirstOrDefault(); + if (typeInfo != null) + { + type = typeInfo.AsType(); + } + } + + if (type == null) + { + throw new InvalidOperationException(String.Format("A type named '{0}' or '{1}' could not be found in assembly '{2}'.", + startupNameWithEnv, + startupNameWithoutEnv, + startupAssemblyName)); + } + + return type; + } + + private static ConfigureBuilder FindConfigureDelegate(Type startupType, string environmentName) + { + var configureMethod = FindMethod(startupType, "Configure{0}", environmentName, typeof(void), required: true); + return new ConfigureBuilder(configureMethod); + } + + private static ConfigureContainerBuilder FindConfigureContainerDelegate(Type startupType, string environmentName) + { + var configureMethod = FindMethod(startupType, "Configure{0}Container", environmentName, typeof(void), required: false); + return new ConfigureContainerBuilder(configureMethod); + } + + private static ConfigureServicesBuilder FindConfigureServicesDelegate(Type startupType, string environmentName) + { + var servicesMethod = FindMethod(startupType, "Configure{0}Services", environmentName, typeof(IServiceProvider), required: false) + ?? FindMethod(startupType, "Configure{0}Services", environmentName, typeof(void), required: false); + return new ConfigureServicesBuilder(servicesMethod); + } + + private static MethodInfo FindMethod(Type startupType, string methodName, string environmentName, Type returnType = null, bool required = true) + { + var methodNameWithEnv = string.Format(CultureInfo.InvariantCulture, methodName, environmentName); + var methodNameWithNoEnv = string.Format(CultureInfo.InvariantCulture, methodName, ""); + + var methods = startupType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static); + var selectedMethods = methods.Where(method => method.Name.Equals(methodNameWithEnv, StringComparison.OrdinalIgnoreCase)).ToList(); + if (selectedMethods.Count > 1) + { + throw new InvalidOperationException(string.Format("Having multiple overloads of method '{0}' is not supported.", methodNameWithEnv)); + } + if (selectedMethods.Count == 0) + { + selectedMethods = methods.Where(method => method.Name.Equals(methodNameWithNoEnv, StringComparison.OrdinalIgnoreCase)).ToList(); + if (selectedMethods.Count > 1) + { + throw new InvalidOperationException(string.Format("Having multiple overloads of method '{0}' is not supported.", methodNameWithNoEnv)); + } + } + + var methodInfo = selectedMethods.FirstOrDefault(); + if (methodInfo == null) + { + if (required) + { + throw new InvalidOperationException(string.Format("A public method named '{0}' or '{1}' could not be found in the '{2}' type.", + methodNameWithEnv, + methodNameWithNoEnv, + startupType.FullName)); + + } + return null; + } + if (returnType != null && methodInfo.ReturnType != returnType) + { + if (required) + { + throw new InvalidOperationException(string.Format("The '{0}' method in the type '{1}' must have a return type of '{2}'.", + methodInfo.Name, + startupType.FullName, + returnType.Name)); + } + return null; + } + return methodInfo; + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Internal/StartupMethods.cs b/src/Hosting/Hosting/src/Internal/StartupMethods.cs new file mode 100644 index 0000000000000000000000000000000000000000..f854c859465c1580fa2424f5dcc27f11294c5544 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/StartupMethods.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public class StartupMethods + { + public StartupMethods(object instance, Action<IApplicationBuilder> configure, Func<IServiceCollection, IServiceProvider> configureServices) + { + Debug.Assert(configure != null); + Debug.Assert(configureServices != null); + + StartupInstance = instance; + ConfigureDelegate = configure; + ConfigureServicesDelegate = configureServices; + } + + public object StartupInstance { get; } + public Func<IServiceCollection, IServiceProvider> ConfigureServicesDelegate { get; } + public Action<IApplicationBuilder> ConfigureDelegate { get; } + + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Internal/WebHost.cs b/src/Hosting/Hosting/src/Internal/WebHost.cs new file mode 100644 index 0000000000000000000000000000000000000000..3764427f63eab087315531cad4317d76a6b89d50 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/WebHost.cs @@ -0,0 +1,362 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Builder; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Hosting.Views; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.StackTrace.Sources; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + internal class WebHost : IWebHost + { + private static readonly string DeprecatedServerUrlsKey = "server.urls"; + + private readonly IServiceCollection _applicationServiceCollection; + private IStartup _startup; + private ApplicationLifetime _applicationLifetime; + private HostedServiceExecutor _hostedServiceExecutor; + + private readonly IServiceProvider _hostingServiceProvider; + private readonly WebHostOptions _options; + private readonly IConfiguration _config; + private readonly AggregateException _hostingStartupErrors; + + private IServiceProvider _applicationServices; + private ExceptionDispatchInfo _applicationServicesException; + private ILogger<WebHost> _logger; + + private bool _stopped; + + // Used for testing only + internal WebHostOptions Options => _options; + + private IServer Server { get; set; } + + public WebHost( + IServiceCollection appServices, + IServiceProvider hostingServiceProvider, + WebHostOptions options, + IConfiguration config, + AggregateException hostingStartupErrors) + { + if (appServices == null) + { + throw new ArgumentNullException(nameof(appServices)); + } + + if (hostingServiceProvider == null) + { + throw new ArgumentNullException(nameof(hostingServiceProvider)); + } + + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + _config = config; + _hostingStartupErrors = hostingStartupErrors; + _options = options; + _applicationServiceCollection = appServices; + _hostingServiceProvider = hostingServiceProvider; + _applicationServiceCollection.AddSingleton<IApplicationLifetime, ApplicationLifetime>(); + // There's no way to to register multiple service types per definition. See https://github.com/aspnet/DependencyInjection/issues/360 + _applicationServiceCollection.AddSingleton(sp => + { + return sp.GetRequiredService<IApplicationLifetime>() as Extensions.Hosting.IApplicationLifetime; + }); + _applicationServiceCollection.AddSingleton<HostedServiceExecutor>(); + } + + public IServiceProvider Services + { + get + { + return _applicationServices; + } + } + + public IFeatureCollection ServerFeatures + { + get + { + EnsureServer(); + return Server?.Features; + } + } + + // Called immediately after the constructor so the properties can rely on it. + public void Initialize() + { + try + { + EnsureApplicationServices(); + } + catch (Exception ex) + { + // EnsureApplicationServices may have failed due to a missing or throwing Startup class. + if (_applicationServices == null) + { + _applicationServices = _applicationServiceCollection.BuildServiceProvider(); + } + + if (!_options.CaptureStartupErrors) + { + throw; + } + + _applicationServicesException = ExceptionDispatchInfo.Capture(ex); + } + } + + public void Start() + { + StartAsync().GetAwaiter().GetResult(); + } + + public virtual async Task StartAsync(CancellationToken cancellationToken = default) + { + HostingEventSource.Log.HostStart(); + _logger = _applicationServices.GetRequiredService<ILogger<WebHost>>(); + _logger.Starting(); + + var application = BuildApplication(); + + _applicationLifetime = _applicationServices.GetRequiredService<IApplicationLifetime>() as ApplicationLifetime; + _hostedServiceExecutor = _applicationServices.GetRequiredService<HostedServiceExecutor>(); + var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticListener>(); + var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>(); + var hostingApp = new HostingApplication(application, _logger, diagnosticSource, httpContextFactory); + await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false); + + // Fire IApplicationLifetime.Started + _applicationLifetime?.NotifyStarted(); + + // Fire IHostedService.Start + await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false); + + _logger.Started(); + + // Log the fact that we did load hosting startup assemblies. + if (_logger.IsEnabled(LogLevel.Debug)) + { + foreach (var assembly in _options.GetFinalHostingStartupAssemblies()) + { + _logger.LogDebug("Loaded hosting startup assembly {assemblyName}", assembly); + } + } + + if (_hostingStartupErrors != null) + { + foreach (var exception in _hostingStartupErrors.InnerExceptions) + { + _logger.HostingStartupAssemblyError(exception); + } + } + } + + private void EnsureApplicationServices() + { + if (_applicationServices == null) + { + EnsureStartup(); + _applicationServices = _startup.ConfigureServices(_applicationServiceCollection); + } + } + + private void EnsureStartup() + { + if (_startup != null) + { + return; + } + + _startup = _hostingServiceProvider.GetService<IStartup>(); + + if (_startup == null) + { + throw new InvalidOperationException($"No startup configured. Please specify startup via WebHostBuilder.UseStartup, WebHostBuilder.Configure, injecting {nameof(IStartup)} or specifying the startup assembly via {nameof(WebHostDefaults.StartupAssemblyKey)} in the web host configuration."); + } + } + + private RequestDelegate BuildApplication() + { + try + { + _applicationServicesException?.Throw(); + EnsureServer(); + + var builderFactory = _applicationServices.GetRequiredService<IApplicationBuilderFactory>(); + var builder = builderFactory.CreateBuilder(Server.Features); + builder.ApplicationServices = _applicationServices; + + var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>(); + Action<IApplicationBuilder> configure = _startup.Configure; + foreach (var filter in startupFilters.Reverse()) + { + configure = filter.Configure(configure); + } + + configure(builder); + + return builder.Build(); + } + catch (Exception ex) + { + if (!_options.SuppressStatusMessages) + { + // Write errors to standard out so they can be retrieved when not in development mode. + Console.WriteLine("Application startup exception: " + ex.ToString()); + } + var logger = _applicationServices.GetRequiredService<ILogger<WebHost>>(); + logger.ApplicationError(ex); + + if (!_options.CaptureStartupErrors) + { + throw; + } + + EnsureServer(); + + // Generate an HTML error page. + var hostingEnv = _applicationServices.GetRequiredService<IHostingEnvironment>(); + var showDetailedErrors = hostingEnv.IsDevelopment() || _options.DetailedErrors; + + var model = new ErrorPageModel + { + RuntimeDisplayName = RuntimeInformation.FrameworkDescription + }; + var systemRuntimeAssembly = typeof(System.ComponentModel.DefaultValueAttribute).GetTypeInfo().Assembly; + var assemblyVersion = new AssemblyName(systemRuntimeAssembly.FullName).Version.ToString(); + var clrVersion = assemblyVersion; + model.RuntimeArchitecture = RuntimeInformation.ProcessArchitecture.ToString(); + var currentAssembly = typeof(ErrorPage).GetTypeInfo().Assembly; + model.CurrentAssemblyVesion = currentAssembly + .GetCustomAttribute<AssemblyInformationalVersionAttribute>() + .InformationalVersion; + model.ClrVersion = clrVersion; + model.OperatingSystemDescription = RuntimeInformation.OSDescription; + + if (showDetailedErrors) + { + var exceptionDetailProvider = new ExceptionDetailsProvider( + hostingEnv.ContentRootFileProvider, + sourceCodeLineCount: 6); + + model.ErrorDetails = exceptionDetailProvider.GetDetails(ex); + } + else + { + model.ErrorDetails = new ExceptionDetails[0]; + } + + var errorPage = new ErrorPage(model); + return context => + { + context.Response.StatusCode = 500; + context.Response.Headers["Cache-Control"] = "no-cache"; + return errorPage.ExecuteAsync(context); + }; + } + } + + private void EnsureServer() + { + if (Server == null) + { + Server = _applicationServices.GetRequiredService<IServer>(); + + var serverAddressesFeature = Server.Features?.Get<IServerAddressesFeature>(); + var addresses = serverAddressesFeature?.Addresses; + if (addresses != null && !addresses.IsReadOnly && addresses.Count == 0) + { + var urls = _config[WebHostDefaults.ServerUrlsKey] ?? _config[DeprecatedServerUrlsKey]; + if (!string.IsNullOrEmpty(urls)) + { + serverAddressesFeature.PreferHostingUrls = WebHostUtilities.ParseBool(_config, WebHostDefaults.PreferHostingUrlsKey); + + foreach (var value in urls.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + addresses.Add(value); + } + } + } + } + } + + public async Task StopAsync(CancellationToken cancellationToken = default) + { + if (_stopped) + { + return; + } + _stopped = true; + + _logger?.Shutdown(); + + var timeoutToken = new CancellationTokenSource(Options.ShutdownTimeout).Token; + if (!cancellationToken.CanBeCanceled) + { + cancellationToken = timeoutToken; + } + else + { + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutToken).Token; + } + + // Fire IApplicationLifetime.Stopping + _applicationLifetime?.StopApplication(); + + if (Server != null) + { + await Server.StopAsync(cancellationToken).ConfigureAwait(false); + } + + // Fire the IHostedService.Stop + if (_hostedServiceExecutor != null) + { + await _hostedServiceExecutor.StopAsync(cancellationToken).ConfigureAwait(false); + } + + // Fire IApplicationLifetime.Stopped + _applicationLifetime?.NotifyStopped(); + + HostingEventSource.Log.HostStop(); + } + + public void Dispose() + { + if (!_stopped) + { + try + { + StopAsync().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + _logger?.ServerShutdownException(ex); + } + } + + (_applicationServices as IDisposable)?.Dispose(); + (_hostingServiceProvider as IDisposable)?.Dispose(); + } + } +} diff --git a/src/Hosting/Hosting/src/Internal/WebHostOptions.cs b/src/Hosting/Hosting/src/Internal/WebHostOptions.cs new file mode 100644 index 0000000000000000000000000000000000000000..e9e611bc699b4ce453d947912e31488bcf8158c2 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/WebHostOptions.cs @@ -0,0 +1,96 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public class WebHostOptions + { + public WebHostOptions() { } + + public WebHostOptions(IConfiguration configuration) + : this(configuration, string.Empty) { } + + public WebHostOptions(IConfiguration configuration, string applicationNameFallback) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + ApplicationName = configuration[WebHostDefaults.ApplicationKey] ?? applicationNameFallback; + StartupAssembly = configuration[WebHostDefaults.StartupAssemblyKey]; + DetailedErrors = WebHostUtilities.ParseBool(configuration, WebHostDefaults.DetailedErrorsKey); + CaptureStartupErrors = WebHostUtilities.ParseBool(configuration, WebHostDefaults.CaptureStartupErrorsKey); + Environment = configuration[WebHostDefaults.EnvironmentKey]; + WebRoot = configuration[WebHostDefaults.WebRootKey]; + ContentRootPath = configuration[WebHostDefaults.ContentRootKey]; + PreventHostingStartup = WebHostUtilities.ParseBool(configuration, WebHostDefaults.PreventHostingStartupKey); + SuppressStatusMessages = WebHostUtilities.ParseBool(configuration, WebHostDefaults.SuppressStatusMessagesKey); + + // Search the primary assembly and configured assemblies. + HostingStartupAssemblies = Split($"{ApplicationName};{configuration[WebHostDefaults.HostingStartupAssembliesKey]}"); + HostingStartupExcludeAssemblies = Split(configuration[WebHostDefaults.HostingStartupExcludeAssembliesKey]); + + var timeout = configuration[WebHostDefaults.ShutdownTimeoutKey]; + if (!string.IsNullOrEmpty(timeout) + && int.TryParse(timeout, NumberStyles.None, CultureInfo.InvariantCulture, out var seconds)) + { + ShutdownTimeout = TimeSpan.FromSeconds(seconds); + } + } + + public string ApplicationName { get; set; } + + public bool PreventHostingStartup { get; set; } + + public bool SuppressStatusMessages { get; set; } + + public IReadOnlyList<string> HostingStartupAssemblies { get; set; } + + public IReadOnlyList<string> HostingStartupExcludeAssemblies { get; set; } + + public bool DetailedErrors { get; set; } + + public bool CaptureStartupErrors { get; set; } + + public string Environment { get; set; } + + public string StartupAssembly { get; set; } + + public string WebRoot { get; set; } + + public string ContentRootPath { get; set; } + + public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5); + + public IEnumerable<string> GetFinalHostingStartupAssemblies() + { + return HostingStartupAssemblies.Except(HostingStartupExcludeAssemblies, StringComparer.OrdinalIgnoreCase); + } + + private IReadOnlyList<string> Split(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return Array.Empty<string>(); + } + + var list = new List<string>(); + foreach (var part in value.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + var trimmedPart = part; + if (!string.IsNullOrEmpty(trimmedPart)) + { + list.Add(trimmedPart); + } + } + return list; + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Internal/WebHostUtilities.cs b/src/Hosting/Hosting/src/Internal/WebHostUtilities.cs new file mode 100644 index 0000000000000000000000000000000000000000..49635699d15ae39a294ade2d34b3004dd027341e --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/WebHostUtilities.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public class WebHostUtilities + { + public static bool ParseBool(IConfiguration configuration, string key) + { + return string.Equals("true", configuration[key], StringComparison.OrdinalIgnoreCase) + || string.Equals("1", configuration[key], StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj new file mode 100644 index 0000000000000000000000000000000000000000..627b36bbc2f155818dc0ab796eb74e8a62609453 --- /dev/null +++ b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj @@ -0,0 +1,30 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>ASP.NET Core hosting infrastructure and startup logic for web applications.</Description> + <TargetFramework>netstandard2.0</TargetFramework> + <NoWarn>$(NoWarn);CS1591</NoWarn> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore;hosting</PackageTags> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" /> + <Reference Include="Microsoft.AspNetCore.Http.Extensions" /> + <Reference Include="Microsoft.AspNetCore.Http" /> + <Reference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" /> + <Reference Include="Microsoft.Extensions.Configuration.FileExtensions" /> + <Reference Include="Microsoft.Extensions.Configuration" /> + <Reference Include="Microsoft.Extensions.DependencyInjection" /> + <Reference Include="Microsoft.Extensions.FileProviders.Physical" /> + <Reference Include="Microsoft.Extensions.Hosting.Abstractions" /> + <Reference Include="Microsoft.Extensions.Logging" /> + <Reference Include="Microsoft.Extensions.Options" /> + <Reference Include="Microsoft.Extensions.RazorViews.Sources" PrivateAssets="All" /> + <Reference Include="Microsoft.Extensions.StackTrace.Sources" PrivateAssets="All" /> + <Reference Include="Microsoft.Extensions.TypeNameHelper.Sources" PrivateAssets="All" /> + <Reference Include="System.Diagnostics.DiagnosticSource" /> + <Reference Include="System.Reflection.Metadata" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/Hosting/src/Properties/AssemblyInfo.cs b/src/Hosting/Hosting/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..39703dc79d2aa03b7ec7552b9c21a4994299c68c --- /dev/null +++ b/src/Hosting/Hosting/src/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Hosting.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Hosting/Hosting/src/Properties/Resources.Designer.cs b/src/Hosting/Hosting/src/Properties/Resources.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..088072729cd2b2b684b42ee9d636befcbb58b3a9 --- /dev/null +++ b/src/Hosting/Hosting/src/Properties/Resources.Designer.cs @@ -0,0 +1,94 @@ +// <auto-generated /> +namespace Microsoft.AspNetCore.Hosting +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Hosting.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// <summary> + /// Internal Server Error + /// </summary> + internal static string ErrorPageHtml_Title + { + get { return GetString("ErrorPageHtml_Title"); } + } + + /// <summary> + /// Internal Server Error + /// </summary> + internal static string FormatErrorPageHtml_Title() + { + return GetString("ErrorPageHtml_Title"); + } + + /// <summary> + /// An error occurred while starting the application. + /// </summary> + internal static string ErrorPageHtml_UnhandledException + { + get { return GetString("ErrorPageHtml_UnhandledException"); } + } + + /// <summary> + /// An error occurred while starting the application. + /// </summary> + internal static string FormatErrorPageHtml_UnhandledException() + { + return GetString("ErrorPageHtml_UnhandledException"); + } + + /// <summary> + /// Unknown location + /// </summary> + internal static string ErrorPageHtml_UnknownLocation + { + get { return GetString("ErrorPageHtml_UnknownLocation"); } + } + + /// <summary> + /// Unknown location + /// </summary> + internal static string FormatErrorPageHtml_UnknownLocation() + { + return GetString("ErrorPageHtml_UnknownLocation"); + } + + /// <summary> + /// WebHostBuilder allows creation only of a single instance of WebHost + /// </summary> + internal static string WebHostBuilder_SingleInstance + { + get { return GetString("WebHostBuilder_SingleInstance"); } + } + + /// <summary> + /// WebHostBuilder allows creation only of a single instance of WebHost + /// </summary> + internal static string FormatWebHostBuilder_SingleInstance() + { + return GetString("WebHostBuilder_SingleInstance"); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Hosting/Hosting/src/Resources.resx b/src/Hosting/Hosting/src/Resources.resx new file mode 100644 index 0000000000000000000000000000000000000000..64d6fe523daeb909cb3472f31511350ec441bfa9 --- /dev/null +++ b/src/Hosting/Hosting/src/Resources.resx @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="ErrorPageHtml_Title" xml:space="preserve"> + <value>Internal Server Error</value> + </data> + <data name="ErrorPageHtml_UnhandledException" xml:space="preserve"> + <value>An error occurred while starting the application.</value> + </data> + <data name="ErrorPageHtml_UnknownLocation" xml:space="preserve"> + <value>Unknown location</value> + </data> + <data name="WebHostBuilder_SingleInstance" xml:space="preserve"> + <value>WebHostBuilder allows creation only of a single instance of WebHost</value> + </data> +</root> \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Server/Features/ServerAddressesFeature.cs b/src/Hosting/Hosting/src/Server/Features/ServerAddressesFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..098ec8cdb0622e58e95e867c5e1ac1bc6f5b881b --- /dev/null +++ b/src/Hosting/Hosting/src/Server/Features/ServerAddressesFeature.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Hosting.Server.Features +{ + public class ServerAddressesFeature : IServerAddressesFeature + { + public ICollection<string> Addresses { get; } = new List<string>(); + + public bool PreferHostingUrls { get; set; } + } +} diff --git a/src/Hosting/Hosting/src/Startup/ConventionBasedStartup.cs b/src/Hosting/Hosting/src/Startup/ConventionBasedStartup.cs new file mode 100644 index 0000000000000000000000000000000000000000..b31f9478d1f6e3be0015cbdd31225c10822b9599 --- /dev/null +++ b/src/Hosting/Hosting/src/Startup/ConventionBasedStartup.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using System.Runtime.ExceptionServices; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting +{ + public class ConventionBasedStartup : IStartup + { + private readonly StartupMethods _methods; + + public ConventionBasedStartup(StartupMethods methods) + { + _methods = methods; + } + + public void Configure(IApplicationBuilder app) + { + try + { + _methods.ConfigureDelegate(app); + } + catch (Exception ex) + { + if (ex is TargetInvocationException) + { + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + } + + throw; + } + } + + public IServiceProvider ConfigureServices(IServiceCollection services) + { + try + { + return _methods.ConfigureServicesDelegate(services); + } + catch (Exception ex) + { + if (ex is TargetInvocationException) + { + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + } + + throw; + } + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Startup/DelegateStartup.cs b/src/Hosting/Hosting/src/Startup/DelegateStartup.cs new file mode 100644 index 0000000000000000000000000000000000000000..d354ad946e5d67d17bec1e86eafd48cb3ad10cc9 --- /dev/null +++ b/src/Hosting/Hosting/src/Startup/DelegateStartup.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting +{ + public class DelegateStartup : StartupBase<IServiceCollection> + { + private Action<IApplicationBuilder> _configureApp; + + public DelegateStartup(IServiceProviderFactory<IServiceCollection> factory, Action<IApplicationBuilder> configureApp) : base(factory) + { + _configureApp = configureApp; + } + + public override void Configure(IApplicationBuilder app) => _configureApp(app); + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.Designer.cs b/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..52a6db45d33d52dc56e89ad7785e16db74c02688 --- /dev/null +++ b/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.Designer.cs @@ -0,0 +1,1055 @@ +namespace Microsoft.AspNetCore.Hosting.Views +{ +#line 1 "ErrorPage.cshtml" +using System + +#line default +#line hidden + ; +#line 2 "ErrorPage.cshtml" +using System.Globalization + +#line default +#line hidden + ; +#line 3 "ErrorPage.cshtml" +using System.Linq + +#line default +#line hidden + ; +#line 4 "ErrorPage.cshtml" +using System.Net + +#line default +#line hidden + ; +#line 5 "ErrorPage.cshtml" +using System.Reflection + +#line default +#line hidden + ; +#line 6 "ErrorPage.cshtml" +using Microsoft.AspNetCore.Hosting.Views + +#line default +#line hidden + ; + using System.Threading.Tasks; + + internal class ErrorPage : Microsoft.Extensions.RazorViews.BaseView + { +#line 9 "ErrorPage.cshtml" + + public ErrorPage(ErrorPageModel model) + { + Model = model; + } + + public ErrorPageModel Model { get; set; } + +#line default +#line hidden + #line hidden + public ErrorPage() + { + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { + WriteLiteral("\r\n"); +#line 17 "ErrorPage.cshtml" + + Response.ContentType = "text/html; charset=utf-8"; + var location = string.Empty; + +#line default +#line hidden + + WriteLiteral("<!DOCTYPE html>\r\n<html"); + BeginWriteAttribute("lang", " lang=\"", 422, "\"", 483, 1); +#line 22 "ErrorPage.cshtml" +WriteAttributeValue("", 429, CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, 429, 54, false); + +#line default +#line hidden + EndWriteAttribute(); + WriteLiteral(" xmlns=\"http://www.w3.org/1999/xhtml\">\r\n <head>\r\n <meta charset=\"utf-8\" />\r\n <title>"); +#line 25 "ErrorPage.cshtml" + Write(Resources.ErrorPageHtml_Title); + +#line default +#line hidden + WriteLiteral(@"</title> + <style> + body { + font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif; + font-size: .813em; + color: #222; +} + +h1, h2, h3, h4, h5 { + /*font-family: 'Segoe UI',Tahoma,Arial,Helvetica,sans-serif;*/ + font-weight: 100; +} + +h1 { + color: #44525e; + margin: 15px 0 15px 0; +} + +h2 { + margin: 10px 5px 0 0; +} + +h3 { + color: #363636; + margin: 5px 5px 0 0; +} + +code { + font-family: Consolas, ""Courier New"", courier, monospace; +} + +body .titleerror { + padding: 3px 3px 6px 3px; + display: block; + font-size: 1.5em; + font-weight: 100; +} + +body .location { + margin: 3px 0 10px 30px; +} + +#header { + font-size: 18px; + padding: 15px 0; + border-top: 1px #ddd solid; + border-bottom: 1px #ddd solid; + margin-bottom: 0; +} + + #header li { + display: inline; + margin: 5px; + padding: 5px; + color: #a0a0a0; + cursor: pointer; + } + + #header .selected { + ba"); + WriteLiteral(@"ckground: #44c5f2; + color: #fff; + } + +#stackpage ul { + list-style: none; + padding-left: 0; + margin: 0; + /*border-bottom: 1px #ddd solid;*/ +} + +#stackpage .details { + font-size: 1.2em; + padding: 3px; + color: #000; +} + +#stackpage .stackerror { + padding: 5px; + border-bottom: 1px #ddd solid; +} + + +#stackpage .frame { + padding: 0; + margin: 0 0 0 30px; +} + + #stackpage .frame h3 { + padding: 2px; + margin: 0; + } + +#stackpage .source { + padding: 0 0 0 30px; +} + + #stackpage .source ol li { + font-family: Consolas, ""Courier New"", courier, monospace; + white-space: pre; + background-color: #fbfbfb; + } + +#stackpage .frame .source .highlight li span { + color: #FF0000; +} + +#stackpage .source ol.collapsible li { + color: #888; +} + + #stackpage .source ol.collapsible li span { + color: #606060; + } + +.page table { + border-collapse: separate; + border-spacing: 0; + margin:"); + WriteLiteral(@" 0 0 20px; +} + +.page th { + vertical-align: bottom; + padding: 10px 5px 5px 5px; + font-weight: 400; + color: #a0a0a0; + text-align: left; +} + +.page td { + padding: 3px 10px; +} + +.page th, .page td { + border-right: 1px #ddd solid; + border-bottom: 1px #ddd solid; + border-left: 1px transparent solid; + border-top: 1px transparent solid; + box-sizing: border-box; +} + + .page th:last-child, .page td:last-child { + border-right: 1px transparent solid; + } + +.page .length { + text-align: right; +} + +a { + color: #1ba1e2; + text-decoration: none; +} + + a:hover { + color: #13709e; + text-decoration: underline; + } + +.showRawException { + cursor: pointer; + color: #44c5f2; + background-color: transparent; + font-size: 1.2em; + text-align: left; + text-decoration: none; + display: inline-block; + border: 0; + padding: 0; +} + +.rawExceptionStackTrace { + font-size: 1.2em; +} + +.rawExceptionBlock { + "); + WriteLiteral(@" border-top: 1px #ddd solid; + border-bottom: 1px #ddd solid; +} + +.showRawExceptionContainer { + margin-top: 10px; + margin-bottom: 10px; +} + +.expandCollapseButton { + cursor: pointer; + float: left; + height: 16px; + width: 16px; + font-size: 10px; + position: absolute; + left: 10px; + background-color: #eee; + padding: 0; + border: 0; + margin: 0; +} + + </style> + </head> + <body> + <h1>"); +#line 226 "ErrorPage.cshtml" + Write(Resources.ErrorPageHtml_UnhandledException); + +#line default +#line hidden + WriteLiteral("</h1>\r\n"); +#line 227 "ErrorPage.cshtml" + + +#line default +#line hidden + +#line 227 "ErrorPage.cshtml" + foreach (var errorDetail in Model.ErrorDetails) + { + +#line default +#line hidden + + WriteLiteral(" <div class=\"titleerror\">"); +#line 229 "ErrorPage.cshtml" + Write(errorDetail.Error.GetType().Name); + +#line default +#line hidden + WriteLiteral(": "); +#line 229 "ErrorPage.cshtml" + Output.Write(HtmlEncodeAndReplaceLineBreaks(errorDetail.Error.Message)); + +#line default +#line hidden + + WriteLiteral("</div>\r\n"); +#line 230 "ErrorPage.cshtml" + + +#line default +#line hidden + +#line 230 "ErrorPage.cshtml" + + var firstFrame = errorDetail.StackFrames.FirstOrDefault(); + if (firstFrame != null) + { + location = firstFrame.Function; + } + + +#line default +#line hidden + +#line 236 "ErrorPage.cshtml" + + if (!string.IsNullOrEmpty(location) && firstFrame != null && !string.IsNullOrEmpty(firstFrame.File)) + { + +#line default +#line hidden + + WriteLiteral(" <p class=\"location\">"); +#line 239 "ErrorPage.cshtml" + Write(location); + +#line default +#line hidden + WriteLiteral(" in <code"); + BeginWriteAttribute("title", " title=\"", 4844, "\"", 4868, 1); +#line 239 "ErrorPage.cshtml" +WriteAttributeValue("", 4852, firstFrame.File, 4852, 16, false); + +#line default +#line hidden + EndWriteAttribute(); + WriteLiteral(">"); +#line 239 "ErrorPage.cshtml" + Write(System.IO.Path.GetFileName(firstFrame.File)); + +#line default +#line hidden + WriteLiteral("</code>, line "); +#line 239 "ErrorPage.cshtml" + Write(firstFrame.Line); + +#line default +#line hidden + WriteLiteral("</p>\r\n"); +#line 240 "ErrorPage.cshtml" + } + else if (!string.IsNullOrEmpty(location)) + { + +#line default +#line hidden + + WriteLiteral(" <p class=\"location\">"); +#line 243 "ErrorPage.cshtml" + Write(location); + +#line default +#line hidden + WriteLiteral("</p>\r\n"); +#line 244 "ErrorPage.cshtml" + } + else + { + +#line default +#line hidden + + WriteLiteral(" <p class=\"location\">"); +#line 247 "ErrorPage.cshtml" + Write(Resources.ErrorPageHtml_UnknownLocation); + +#line default +#line hidden + WriteLiteral("</p>\r\n"); +#line 248 "ErrorPage.cshtml" + } + + var reflectionTypeLoadException = errorDetail.Error as ReflectionTypeLoadException; + if (reflectionTypeLoadException != null) + { + if (reflectionTypeLoadException.LoaderExceptions.Length > 0) + { + +#line default +#line hidden + + WriteLiteral(" <h3>Loader Exceptions:</h3>\r\n <ul>\r\n"); +#line 257 "ErrorPage.cshtml" + + +#line default +#line hidden + +#line 257 "ErrorPage.cshtml" + foreach (var ex in reflectionTypeLoadException.LoaderExceptions) + { + +#line default +#line hidden + + WriteLiteral(" <li>"); +#line 259 "ErrorPage.cshtml" + Write(ex.Message); + +#line default +#line hidden + WriteLiteral("</li>\r\n"); +#line 260 "ErrorPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral(" </ul>\r\n"); +#line 262 "ErrorPage.cshtml" + } + } + } + +#line default +#line hidden + + WriteLiteral(" <div id=\"stackpage\" class=\"page\">\r\n <ul>\r\n"); +#line 267 "ErrorPage.cshtml" + + +#line default +#line hidden + +#line 267 "ErrorPage.cshtml" + + var exceptionCount = 0; + var stackFrameCount = 0; + var exceptionDetailId = ""; + var frameId = ""; + + +#line default +#line hidden + + WriteLiteral(" "); +#line 273 "ErrorPage.cshtml" + foreach (var errorDetail in Model.ErrorDetails) + { + + +#line default +#line hidden + +#line 275 "ErrorPage.cshtml" + + exceptionCount++; + exceptionDetailId = "exceptionDetail" + exceptionCount; + + +#line default +#line hidden + +#line 278 "ErrorPage.cshtml" + + +#line default +#line hidden + + WriteLiteral(" <li>\r\n <h2 class=\"stackerror\">"); +#line 280 "ErrorPage.cshtml" + Write(errorDetail.Error.GetType().Name); + +#line default +#line hidden + WriteLiteral(": "); +#line 280 "ErrorPage.cshtml" + Write(errorDetail.Error.Message); + +#line default +#line hidden + WriteLiteral("</h2>\r\n <ul>\r\n"); +#line 282 "ErrorPage.cshtml" + + +#line default +#line hidden + +#line 282 "ErrorPage.cshtml" + foreach (var frame in errorDetail.StackFrames) + { + + +#line default +#line hidden + +#line 284 "ErrorPage.cshtml" + + stackFrameCount++; + frameId = "frame" + stackFrameCount; + + +#line default +#line hidden + +#line 287 "ErrorPage.cshtml" + + +#line default +#line hidden + + WriteLiteral(" <li class=\"frame\""); + BeginWriteAttribute("id", " id=\"", 6874, "\"", 6887, 1); +#line 288 "ErrorPage.cshtml" +WriteAttributeValue("", 6879, frameId, 6879, 8, false); + +#line default +#line hidden + EndWriteAttribute(); + WriteLiteral(">\r\n"); +#line 289 "ErrorPage.cshtml" + + +#line default +#line hidden + +#line 289 "ErrorPage.cshtml" + if (string.IsNullOrEmpty(frame.File)) + { + +#line default +#line hidden + + WriteLiteral(" <h3>"); +#line 291 "ErrorPage.cshtml" + Write(frame.Function); + +#line default +#line hidden + WriteLiteral("</h3>\r\n"); +#line 292 "ErrorPage.cshtml" + } + else + { + +#line default +#line hidden + + WriteLiteral(" <h3>"); +#line 295 "ErrorPage.cshtml" + Write(frame.Function); + +#line default +#line hidden + WriteLiteral(" in <code"); + BeginWriteAttribute("title", " title=\"", 7232, "\"", 7251, 1); +#line 295 "ErrorPage.cshtml" +WriteAttributeValue("", 7240, frame.File, 7240, 11, false); + +#line default +#line hidden + EndWriteAttribute(); + WriteLiteral(">"); +#line 295 "ErrorPage.cshtml" + Write(System.IO.Path.GetFileName(frame.File)); + +#line default +#line hidden + WriteLiteral("</code></h3>\r\n"); +#line 296 "ErrorPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral("\r\n"); +#line 298 "ErrorPage.cshtml" + + +#line default +#line hidden + +#line 298 "ErrorPage.cshtml" + if (frame.Line != 0 && frame.ContextCode.Any()) + { + +#line default +#line hidden + + WriteLiteral(" <button class=\"expandCollapseButton\" data-frameId=\""); +#line 300 "ErrorPage.cshtml" + Write(frameId); + +#line default +#line hidden + WriteLiteral("\">+</button>\r\n <div class=\"source\">\r\n"); +#line 302 "ErrorPage.cshtml" + + +#line default +#line hidden + +#line 302 "ErrorPage.cshtml" + if (frame.PreContextCode.Any()) + { + +#line default +#line hidden + + WriteLiteral(" <ol"); + BeginWriteAttribute("start", " start=\"", 7791, "\"", 7820, 1); +#line 304 "ErrorPage.cshtml" +WriteAttributeValue("", 7799, frame.PreContextLine, 7799, 21, false); + +#line default +#line hidden + EndWriteAttribute(); + WriteLiteral(" class=\"collapsible\">\r\n"); +#line 305 "ErrorPage.cshtml" + + +#line default +#line hidden + +#line 305 "ErrorPage.cshtml" + foreach (var line in frame.PreContextCode) + { + +#line default +#line hidden + + WriteLiteral(" <li><span>"); +#line 307 "ErrorPage.cshtml" + Write(line); + +#line default +#line hidden + WriteLiteral("</span></li>\r\n"); +#line 308 "ErrorPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral(" </ol>\r\n"); +#line 310 "ErrorPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral("\r\n <ol"); + BeginWriteAttribute("start", " start=\"", 8259, "\"", 8278, 1); +#line 312 "ErrorPage.cshtml" +WriteAttributeValue("", 8267, frame.Line, 8267, 11, false); + +#line default +#line hidden + EndWriteAttribute(); + WriteLiteral(" class=\"highlight\">\r\n"); +#line 313 "ErrorPage.cshtml" + + +#line default +#line hidden + +#line 313 "ErrorPage.cshtml" + foreach (var line in frame.ContextCode) + { + +#line default +#line hidden + + WriteLiteral(" <li><span>"); +#line 315 "ErrorPage.cshtml" + Write(line); + +#line default +#line hidden + WriteLiteral("</span></li>\r\n"); +#line 316 "ErrorPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral(" </ol>\r\n\r\n"); +#line 319 "ErrorPage.cshtml" + + +#line default +#line hidden + +#line 319 "ErrorPage.cshtml" + if (frame.PostContextCode.Any()) + { + +#line default +#line hidden + + WriteLiteral(" <ol"); + BeginWriteAttribute("start", " start=\'", 8771, "\'", 8796, 1); +#line 321 "ErrorPage.cshtml" +WriteAttributeValue("", 8779, frame.Line + 1, 8779, 17, false); + +#line default +#line hidden + EndWriteAttribute(); + WriteLiteral(" class=\"collapsible\">\r\n"); +#line 322 "ErrorPage.cshtml" + + +#line default +#line hidden + +#line 322 "ErrorPage.cshtml" + foreach (var line in frame.PostContextCode) + { + +#line default +#line hidden + + WriteLiteral(" <li><span>"); +#line 324 "ErrorPage.cshtml" + Write(line); + +#line default +#line hidden + WriteLiteral("</span></li>\r\n"); +#line 325 "ErrorPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral(" </ol>\r\n"); +#line 327 "ErrorPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral(" </div>\r\n"); +#line 329 "ErrorPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral(" </li>\r\n"); +#line 331 "ErrorPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral(@" </ul> + </li> + <li> + <br/> + <div class=""rawExceptionBlock""> + <div class=""showRawExceptionContainer""> + <button class=""showRawException"" data-exceptionDetailId="""); +#line 338 "ErrorPage.cshtml" + Write(exceptionDetailId); + +#line default +#line hidden + WriteLiteral("\">Show raw exception details</button>\r\n </div>\r\n <div"); + BeginWriteAttribute("id", " id=\"", 9787, "\"", 9810, 1); +#line 340 "ErrorPage.cshtml" +WriteAttributeValue("", 9792, exceptionDetailId, 9792, 18, false); + +#line default +#line hidden + EndWriteAttribute(); + WriteLiteral(" class=\"rawExceptionDetails\">\r\n <pre class=\"rawExceptionStackTrace\">"); +#line 341 "ErrorPage.cshtml" + Write(errorDetail.Error.ToString()); + +#line default +#line hidden + WriteLiteral("</pre>\r\n </div>\r\n </div>\r\n </li>\r\n"); +#line 345 "ErrorPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral(" </ul>\r\n </div>\r\n <footer>\r\n "); +#line 349 "ErrorPage.cshtml" + Write(Model.RuntimeDisplayName); + +#line default +#line hidden + WriteLiteral(" "); +#line 349 "ErrorPage.cshtml" + Write(Model.RuntimeArchitecture); + +#line default +#line hidden + WriteLiteral(" v"); +#line 349 "ErrorPage.cshtml" + Write(Model.ClrVersion); + +#line default +#line hidden + WriteLiteral(" | Microsoft.AspNetCore.Hosting version "); +#line 349 "ErrorPage.cshtml" + Write(Model.CurrentAssemblyVesion); + +#line default +#line hidden + WriteLiteral(" | "); +#line 349 "ErrorPage.cshtml" + Write(Model.OperatingSystemDescription); + +#line default +#line hidden + WriteLiteral(@" | <a href=""http://go.microsoft.com/fwlink/?LinkId=517394"">Need help?</a> + </footer> + <script> + //<!-- + (function (window, undefined) { + ""use strict""; + + function ns(selector, element) { + return new NodeCollection(selector, element); + } + + function NodeCollection(selector, element) { + this.items = []; + element = element || window.document; + + var nodeList; + + if (typeof (selector) === ""string"") { + nodeList = element.querySelectorAll(selector); + for (var i = 0, l = nodeList.length; i < l; i++) { + this.items.push(nodeList.item(i)); + } + } + } + + NodeCollection.prototype = { + each: function (callback) { + for (var i = 0, l = this.items.length; i < l; i++) { + callback(this.items[i], i); + } + return this; + }, + + children: function (selector) { + "); + WriteLiteral(@" var children = []; + + this.each(function (el) { + children = children.concat(ns(selector, el).items); + }); + + return ns(children); + }, + + hide: function () { + this.each(function (el) { + el.style.display = ""none""; + }); + + return this; + }, + + toggle: function () { + this.each(function (el) { + el.style.display = el.style.display === ""none"" ? """" : ""none""; + }); + + return this; + }, + + show: function () { + this.each(function (el) { + el.style.display = """"; + }); + + return this; + }, + + addClass: function (className) { + this.each(function (el) { + var existingClassName = el.className, + classNames; + if (!existingClassName) { + el.className = className; + "); + WriteLiteral(@" } else { + classNames = existingClassName.split("" ""); + if (classNames.indexOf(className) < 0) { + el.className = existingClassName + "" "" + className; + } + } + }); + + return this; + }, + + removeClass: function (className) { + this.each(function (el) { + var existingClassName = el.className, + classNames, index; + if (existingClassName === className) { + el.className = """"; + } else if (existingClassName) { + classNames = existingClassName.split("" ""); + index = classNames.indexOf(className); + if (index > 0) { + classNames.splice(index, 1); + el.className = classNames.join("" ""); + } + } + }); + + return this; + }, + + "); + WriteLiteral(@" attr: function (name) { + if (this.items.length === 0) { + return null; + } + + return this.items[0].getAttribute(name); + }, + + on: function (eventName, handler) { + this.each(function (el, idx) { + var callback = function (e) { + e = e || window.event; + if (!e.which && e.keyCode) { + e.which = e.keyCode; // Normalize IE8 key events + } + handler.apply(el, [e]); + }; + + if (el.addEventListener) { // DOM Events + el.addEventListener(eventName, callback, false); + } else if (el.attachEvent) { // IE8 events + el.attachEvent(""on"" + eventName, callback); + } else { + el[""on"" + type] = callback; + } + }); + + return this; + }, + + click: function (handler) { "); + WriteLiteral(@" + return this.on(""click"", handler); + }, + + keypress: function (handler) { + return this.on(""keypress"", handler); + } + }; + + function frame(el) { + ns("".source .collapsible"", el).toggle(); + } + + function expandCollapseButton(el) { + var frameId = el.getAttribute(""data-frameId""); + frame(document.getElementById(frameId)); + if (el.innerText === ""+"") { + el.innerText = ""-""; + } + else { + el.innerText = ""+""; + } + } + + function tab(el) { + var unselected = ns(""#header .selected"").removeClass(""selected"").attr(""id""); + var selected = ns(""#"" + el.id).addClass(""selected"").attr(""id""); + + ns(""#"" + unselected + ""page"").hide(); + ns(""#"" + selected + ""page"").show(); + } + + ns("".rawExceptionDetails"").hide(); + ns("".collapsible"").hide(); + ns("".page"").hide(); + ns(""#stackpage"").show(); + + ns("".expandCollapseButton"") + .click(functi"); + WriteLiteral(@"on () { + expandCollapseButton(this); + }) + .keypress(function (e) { + if (e.which === 13) { + expandCollapseButton(this); + } + }); + + ns(""#header li"") + .click(function () { + tab(this); + }) + .keypress(function (e) { + if (e.which === 13) { + tab(this); + } + }); + + ns("".showRawException"") + .click(function () { + var exceptionDetailId = this.getAttribute(""data-exceptionDetailId""); + ns(""#"" + exceptionDetailId).toggle(); + }); +})(window); + //--> + </script> +</body> +</html> +"); + } + #pragma warning restore 1998 + } +} diff --git a/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.cshtml b/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.cshtml new file mode 100644 index 0000000000000000000000000000000000000000..8f8360c565f873fddd87dc70eb4ebb6f93fa6b36 --- /dev/null +++ b/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.cshtml @@ -0,0 +1,162 @@ +@using System +@using System.Globalization +@using System.Linq +@using System.Net +@using System.Reflection +@using Microsoft.AspNetCore.Hosting.Views + +@functions +{ + public ErrorPage(ErrorPageModel model) + { + Model = model; + } + + public ErrorPageModel Model { get; set; } +} +@{ + Response.ContentType = "text/html; charset=utf-8"; + var location = string.Empty; +} +<!DOCTYPE html> +<html lang="@CultureInfo.CurrentUICulture.TwoLetterISOLanguageName" xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf-8" /> + <title>@Resources.ErrorPageHtml_Title</title> + <style> + <%$ include: ErrorPage.css %> + </style> + </head> + <body> + <h1>@Resources.ErrorPageHtml_UnhandledException</h1> + @foreach (var errorDetail in Model.ErrorDetails) + { + <div class="titleerror">@errorDetail.Error.GetType().Name: @{ Output.Write(HtmlEncodeAndReplaceLineBreaks(errorDetail.Error.Message)); }</div> + @{ + var firstFrame = errorDetail.StackFrames.FirstOrDefault(); + if (firstFrame != null) + { + location = firstFrame.Function; + } + } + if (!string.IsNullOrEmpty(location) && firstFrame != null && !string.IsNullOrEmpty(firstFrame.File)) + { + <p class="location">@location in <code title="@firstFrame.File">@System.IO.Path.GetFileName(firstFrame.File)</code>, line @firstFrame.Line</p> + } + else if (!string.IsNullOrEmpty(location)) + { + <p class="location">@location</p> + } + else + { + <p class="location">@Resources.ErrorPageHtml_UnknownLocation</p> + } + + var reflectionTypeLoadException = errorDetail.Error as ReflectionTypeLoadException; + if (reflectionTypeLoadException != null) + { + if (reflectionTypeLoadException.LoaderExceptions.Length > 0) + { + <h3>Loader Exceptions:</h3> + <ul> + @foreach (var ex in reflectionTypeLoadException.LoaderExceptions) + { + <li>@ex.Message</li> + } + </ul> + } + } + } + <div id="stackpage" class="page"> + <ul> + @{ + var exceptionCount = 0; + var stackFrameCount = 0; + var exceptionDetailId = ""; + var frameId = ""; + } + @foreach (var errorDetail in Model.ErrorDetails) + { + @{ + exceptionCount++; + exceptionDetailId = "exceptionDetail" + exceptionCount; + } + <li> + <h2 class="stackerror">@errorDetail.Error.GetType().Name: @errorDetail.Error.Message</h2> + <ul> + @foreach (var frame in errorDetail.StackFrames) + { + @{ + stackFrameCount++; + frameId = "frame" + stackFrameCount; + } + <li class="frame" id="@frameId"> + @if (string.IsNullOrEmpty(frame.File)) + { + <h3>@frame.Function</h3> + } + else + { + <h3>@frame.Function in <code title="@frame.File">@System.IO.Path.GetFileName(frame.File)</code></h3> + } + + @if (frame.Line != 0 && frame.ContextCode.Any()) + { + <button class="expandCollapseButton" data-frameId="@frameId">+</button> + <div class="source"> + @if (frame.PreContextCode.Any()) + { + <ol start="@frame.PreContextLine" class="collapsible"> + @foreach (var line in frame.PreContextCode) + { + <li><span>@line</span></li> + } + </ol> + } + + <ol start="@frame.Line" class="highlight"> + @foreach (var line in frame.ContextCode) + { + <li><span>@line</span></li> + } + </ol> + + @if (frame.PostContextCode.Any()) + { + <ol start='@(frame.Line + 1)' class="collapsible"> + @foreach (var line in frame.PostContextCode) + { + <li><span>@line</span></li> + } + </ol> + } + </div> + } + </li> + } + </ul> + </li> + <li> + <br/> + <div class="rawExceptionBlock"> + <div class="showRawExceptionContainer"> + <button class="showRawException" data-exceptionDetailId="@exceptionDetailId">Show raw exception details</button> + </div> + <div id="@exceptionDetailId" class="rawExceptionDetails"> + <pre class="rawExceptionStackTrace">@errorDetail.Error.ToString()</pre> + </div> + </div> + </li> + } + </ul> + </div> + <footer> + @Model.RuntimeDisplayName @Model.RuntimeArchitecture v@(Model.ClrVersion) | Microsoft.AspNetCore.Hosting version @Model.CurrentAssemblyVesion | @Model.OperatingSystemDescription | <a href="http://go.microsoft.com/fwlink/?LinkId=517394">Need help?</a> + </footer> + <script> + //<!-- + <%$ include: ErrorPage.js %> + //--> + </script> +</body> +</html> diff --git a/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.css b/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.css new file mode 100644 index 0000000000000000000000000000000000000000..4d3287c12dd6c08e026d617358b17c1f3fefb1d1 --- /dev/null +++ b/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.css @@ -0,0 +1,195 @@ +body { + font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif; + font-size: .813em; + color: #222; +} + +h1, h2, h3, h4, h5 { + /*font-family: 'Segoe UI',Tahoma,Arial,Helvetica,sans-serif;*/ + font-weight: 100; +} + +h1 { + color: #44525e; + margin: 15px 0 15px 0; +} + +h2 { + margin: 10px 5px 0 0; +} + +h3 { + color: #363636; + margin: 5px 5px 0 0; +} + +code { + font-family: Consolas, "Courier New", courier, monospace; +} + +body .titleerror { + padding: 3px 3px 6px 3px; + display: block; + font-size: 1.5em; + font-weight: 100; +} + +body .location { + margin: 3px 0 10px 30px; +} + +#header { + font-size: 18px; + padding: 15px 0; + border-top: 1px #ddd solid; + border-bottom: 1px #ddd solid; + margin-bottom: 0; +} + + #header li { + display: inline; + margin: 5px; + padding: 5px; + color: #a0a0a0; + cursor: pointer; + } + + #header .selected { + background: #44c5f2; + color: #fff; + } + +#stackpage ul { + list-style: none; + padding-left: 0; + margin: 0; + /*border-bottom: 1px #ddd solid;*/ +} + +#stackpage .details { + font-size: 1.2em; + padding: 3px; + color: #000; +} + +#stackpage .stackerror { + padding: 5px; + border-bottom: 1px #ddd solid; +} + + +#stackpage .frame { + padding: 0; + margin: 0 0 0 30px; +} + + #stackpage .frame h3 { + padding: 2px; + margin: 0; + } + +#stackpage .source { + padding: 0 0 0 30px; +} + + #stackpage .source ol li { + font-family: Consolas, "Courier New", courier, monospace; + white-space: pre; + background-color: #fbfbfb; + } + +#stackpage .frame .source .highlight li span { + color: #FF0000; +} + +#stackpage .source ol.collapsible li { + color: #888; +} + + #stackpage .source ol.collapsible li span { + color: #606060; + } + +.page table { + border-collapse: separate; + border-spacing: 0; + margin: 0 0 20px; +} + +.page th { + vertical-align: bottom; + padding: 10px 5px 5px 5px; + font-weight: 400; + color: #a0a0a0; + text-align: left; +} + +.page td { + padding: 3px 10px; +} + +.page th, .page td { + border-right: 1px #ddd solid; + border-bottom: 1px #ddd solid; + border-left: 1px transparent solid; + border-top: 1px transparent solid; + box-sizing: border-box; +} + + .page th:last-child, .page td:last-child { + border-right: 1px transparent solid; + } + +.page .length { + text-align: right; +} + +a { + color: #1ba1e2; + text-decoration: none; +} + + a:hover { + color: #13709e; + text-decoration: underline; + } + +.showRawException { + cursor: pointer; + color: #44c5f2; + background-color: transparent; + font-size: 1.2em; + text-align: left; + text-decoration: none; + display: inline-block; + border: 0; + padding: 0; +} + +.rawExceptionStackTrace { + font-size: 1.2em; +} + +.rawExceptionBlock { + border-top: 1px #ddd solid; + border-bottom: 1px #ddd solid; +} + +.showRawExceptionContainer { + margin-top: 10px; + margin-bottom: 10px; +} + +.expandCollapseButton { + cursor: pointer; + float: left; + height: 16px; + width: 16px; + font-size: 10px; + position: absolute; + left: 10px; + background-color: #eee; + padding: 0; + border: 0; + margin: 0; +} diff --git a/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.js b/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.js new file mode 100644 index 0000000000000000000000000000000000000000..3925cfd2f2759d2b97fe92276504a28dc5812985 --- /dev/null +++ b/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.js @@ -0,0 +1,192 @@ +(function (window, undefined) { + "use strict"; + + function ns(selector, element) { + return new NodeCollection(selector, element); + } + + function NodeCollection(selector, element) { + this.items = []; + element = element || window.document; + + var nodeList; + + if (typeof (selector) === "string") { + nodeList = element.querySelectorAll(selector); + for (var i = 0, l = nodeList.length; i < l; i++) { + this.items.push(nodeList.item(i)); + } + } + } + + NodeCollection.prototype = { + each: function (callback) { + for (var i = 0, l = this.items.length; i < l; i++) { + callback(this.items[i], i); + } + return this; + }, + + children: function (selector) { + var children = []; + + this.each(function (el) { + children = children.concat(ns(selector, el).items); + }); + + return ns(children); + }, + + hide: function () { + this.each(function (el) { + el.style.display = "none"; + }); + + return this; + }, + + toggle: function () { + this.each(function (el) { + el.style.display = el.style.display === "none" ? "" : "none"; + }); + + return this; + }, + + show: function () { + this.each(function (el) { + el.style.display = ""; + }); + + return this; + }, + + addClass: function (className) { + this.each(function (el) { + var existingClassName = el.className, + classNames; + if (!existingClassName) { + el.className = className; + } else { + classNames = existingClassName.split(" "); + if (classNames.indexOf(className) < 0) { + el.className = existingClassName + " " + className; + } + } + }); + + return this; + }, + + removeClass: function (className) { + this.each(function (el) { + var existingClassName = el.className, + classNames, index; + if (existingClassName === className) { + el.className = ""; + } else if (existingClassName) { + classNames = existingClassName.split(" "); + index = classNames.indexOf(className); + if (index > 0) { + classNames.splice(index, 1); + el.className = classNames.join(" "); + } + } + }); + + return this; + }, + + attr: function (name) { + if (this.items.length === 0) { + return null; + } + + return this.items[0].getAttribute(name); + }, + + on: function (eventName, handler) { + this.each(function (el, idx) { + var callback = function (e) { + e = e || window.event; + if (!e.which && e.keyCode) { + e.which = e.keyCode; // Normalize IE8 key events + } + handler.apply(el, [e]); + }; + + if (el.addEventListener) { // DOM Events + el.addEventListener(eventName, callback, false); + } else if (el.attachEvent) { // IE8 events + el.attachEvent("on" + eventName, callback); + } else { + el["on" + type] = callback; + } + }); + + return this; + }, + + click: function (handler) { + return this.on("click", handler); + }, + + keypress: function (handler) { + return this.on("keypress", handler); + } + }; + + function frame(el) { + ns(".source .collapsible", el).toggle(); + } + + function expandCollapseButton(el) { + var frameId = el.getAttribute("data-frameId"); + frame(document.getElementById(frameId)); + if (el.innerText === "+") { + el.innerText = "-"; + } + else { + el.innerText = "+"; + } + } + + function tab(el) { + var unselected = ns("#header .selected").removeClass("selected").attr("id"); + var selected = ns("#" + el.id).addClass("selected").attr("id"); + + ns("#" + unselected + "page").hide(); + ns("#" + selected + "page").show(); + } + + ns(".rawExceptionDetails").hide(); + ns(".collapsible").hide(); + ns(".page").hide(); + ns("#stackpage").show(); + + ns(".expandCollapseButton") + .click(function () { + expandCollapseButton(this); + }) + .keypress(function (e) { + if (e.which === 13) { + expandCollapseButton(this); + } + }); + + ns("#header li") + .click(function () { + tab(this); + }) + .keypress(function (e) { + if (e.which === 13) { + tab(this); + } + }); + + ns(".showRawException") + .click(function () { + var exceptionDetailId = this.getAttribute("data-exceptionDetailId"); + ns("#" + exceptionDetailId).toggle(); + }); +})(window); \ No newline at end of file diff --git a/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPageModel.cs b/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPageModel.cs new file mode 100644 index 0000000000000000000000000000000000000000..b0a9f0354a4c88b8c43d3ec94eb80e8dd1facd5a --- /dev/null +++ b/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPageModel.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.Extensions.StackTrace.Sources; + +namespace Microsoft.AspNetCore.Hosting.Views +{ + /// <summary> + /// Holds data to be displayed on the error page. + /// </summary> + internal class ErrorPageModel + { + /// <summary> + /// Detailed information about each exception in the stack. + /// </summary> + public IEnumerable<ExceptionDetails> ErrorDetails { get; set; } + + public string RuntimeDisplayName { get; set; } + + public string RuntimeArchitecture { get; set; } + + public string ClrVersion { get; set; } + + public string CurrentAssemblyVesion { get; set; } + + public string OperatingSystemDescription { get; set; } + } +} diff --git a/src/Hosting/Hosting/src/Startup/StartupBase.cs b/src/Hosting/Hosting/src/Startup/StartupBase.cs new file mode 100644 index 0000000000000000000000000000000000000000..5a180ba9b422005016138acf9fbf6ff6fb6254a4 --- /dev/null +++ b/src/Hosting/Hosting/src/Startup/StartupBase.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting +{ + public abstract class StartupBase : IStartup + { + public abstract void Configure(IApplicationBuilder app); + + IServiceProvider IStartup.ConfigureServices(IServiceCollection services) + { + ConfigureServices(services); + return CreateServiceProvider(services); + } + + public virtual void ConfigureServices(IServiceCollection services) + { + } + + public virtual IServiceProvider CreateServiceProvider(IServiceCollection services) + { + return services.BuildServiceProvider(); + } + } + + public abstract class StartupBase<TBuilder> : StartupBase + { + private readonly IServiceProviderFactory<TBuilder> _factory; + + public StartupBase(IServiceProviderFactory<TBuilder> factory) + { + _factory = factory; + } + + public override IServiceProvider CreateServiceProvider(IServiceCollection services) + { + var builder = _factory.CreateBuilder(services); + ConfigureContainer(builder); + return _factory.CreateServiceProvider(builder); + } + + public virtual void ConfigureContainer(TBuilder builder) + { + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/WebHostBuilder.cs b/src/Hosting/Hosting/src/WebHostBuilder.cs new file mode 100644 index 0000000000000000000000000000000000000000..423b898cec5afd3a23625ba2baada19f85160869 --- /dev/null +++ b/src/Hosting/Hosting/src/WebHostBuilder.cs @@ -0,0 +1,364 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using Microsoft.AspNetCore.Hosting.Builder; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Hosting +{ + /// <summary> + /// A builder for <see cref="IWebHost"/> + /// </summary> + public class WebHostBuilder : IWebHostBuilder + { + private readonly HostingEnvironment _hostingEnvironment; + private readonly List<Action<WebHostBuilderContext, IServiceCollection>> _configureServicesDelegates; + + private IConfiguration _config; + private WebHostOptions _options; + private WebHostBuilderContext _context; + private bool _webHostBuilt; + private List<Action<WebHostBuilderContext, IConfigurationBuilder>> _configureAppConfigurationBuilderDelegates; + + /// <summary> + /// Initializes a new instance of the <see cref="WebHostBuilder"/> class. + /// </summary> + public WebHostBuilder() + { + _hostingEnvironment = new HostingEnvironment(); + _configureServicesDelegates = new List<Action<WebHostBuilderContext, IServiceCollection>>(); + _configureAppConfigurationBuilderDelegates = new List<Action<WebHostBuilderContext, IConfigurationBuilder>>(); + + _config = new ConfigurationBuilder() + .AddEnvironmentVariables(prefix: "ASPNETCORE_") + .Build(); + + if (string.IsNullOrEmpty(GetSetting(WebHostDefaults.EnvironmentKey))) + { + // Try adding legacy environment keys, never remove these. + UseSetting(WebHostDefaults.EnvironmentKey, Environment.GetEnvironmentVariable("Hosting:Environment") + ?? Environment.GetEnvironmentVariable("ASPNET_ENV")); + } + + if (string.IsNullOrEmpty(GetSetting(WebHostDefaults.ServerUrlsKey))) + { + // Try adding legacy url key, never remove this. + UseSetting(WebHostDefaults.ServerUrlsKey, Environment.GetEnvironmentVariable("ASPNETCORE_SERVER.URLS")); + } + + _context = new WebHostBuilderContext + { + Configuration = _config + }; + } + + /// <summary> + /// Get the setting value from the configuration. + /// </summary> + /// <param name="key">The key of the setting to look up.</param> + /// <returns>The value the setting currently contains.</returns> + public string GetSetting(string key) + { + return _config[key]; + } + + /// <summary> + /// Add or replace a setting in the configuration. + /// </summary> + /// <param name="key">The key of the setting to add or replace.</param> + /// <param name="value">The value of the setting to add or replace.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public IWebHostBuilder UseSetting(string key, string value) + { + _config[key] = value; + return this; + } + + /// <summary> + /// Adds a delegate for configuring additional services for the host or web application. This may be called + /// multiple times. + /// </summary> + /// <param name="configureServices">A delegate for configuring the <see cref="IServiceCollection"/>.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices) + { + if (configureServices == null) + { + throw new ArgumentNullException(nameof(configureServices)); + } + + return ConfigureServices((_, services) => configureServices(services)); + } + + /// <summary> + /// Adds a delegate for configuring additional services for the host or web application. This may be called + /// multiple times. + /// </summary> + /// <param name="configureServices">A delegate for configuring the <see cref="IServiceCollection"/>.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public IWebHostBuilder ConfigureServices(Action<WebHostBuilderContext, IServiceCollection> configureServices) + { + if (configureServices == null) + { + throw new ArgumentNullException(nameof(configureServices)); + } + + _configureServicesDelegates.Add(configureServices); + return this; + } + + /// <summary> + /// Adds a delegate for configuring the <see cref="IConfigurationBuilder"/> that will construct an <see cref="IConfiguration"/>. + /// </summary> + /// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder" /> that will be used to construct an <see cref="IConfiguration" />.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + /// <remarks> + /// The <see cref="IConfiguration"/> and <see cref="ILoggerFactory"/> on the <see cref="WebHostBuilderContext"/> are uninitialized at this stage. + /// The <see cref="IConfigurationBuilder"/> is pre-populated with the settings of the <see cref="IWebHostBuilder"/>. + /// </remarks> + public IWebHostBuilder ConfigureAppConfiguration(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate) + { + if (configureDelegate == null) + { + throw new ArgumentNullException(nameof(configureDelegate)); + } + + _configureAppConfigurationBuilderDelegates.Add(configureDelegate); + return this; + } + + /// <summary> + /// Builds the required services and an <see cref="IWebHost"/> which hosts a web application. + /// </summary> + public IWebHost Build() + { + if (_webHostBuilt) + { + throw new InvalidOperationException(Resources.WebHostBuilder_SingleInstance); + } + _webHostBuilt = true; + + var hostingServices = BuildCommonServices(out var hostingStartupErrors); + var applicationServices = hostingServices.Clone(); + var hostingServiceProvider = GetProviderFromFactory(hostingServices); + + if (!_options.SuppressStatusMessages) + { + // Warn about deprecated environment variables + if (Environment.GetEnvironmentVariable("Hosting:Environment") != null) + { + Console.WriteLine("The environment variable 'Hosting:Environment' is obsolete and has been replaced with 'ASPNETCORE_ENVIRONMENT'"); + } + + if (Environment.GetEnvironmentVariable("ASPNET_ENV") != null) + { + Console.WriteLine("The environment variable 'ASPNET_ENV' is obsolete and has been replaced with 'ASPNETCORE_ENVIRONMENT'"); + } + + if (Environment.GetEnvironmentVariable("ASPNETCORE_SERVER.URLS") != null) + { + Console.WriteLine("The environment variable 'ASPNETCORE_SERVER.URLS' is obsolete and has been replaced with 'ASPNETCORE_URLS'"); + } + } + + var logger = hostingServiceProvider.GetRequiredService<ILogger<WebHost>>(); + // Warn about duplicate HostingStartupAssemblies + foreach (var assemblyName in _options.GetFinalHostingStartupAssemblies().GroupBy(a => a, StringComparer.OrdinalIgnoreCase).Where(g => g.Count() > 1)) + { + logger.LogWarning($"The assembly {assemblyName} was specified multiple times. Hosting startup assemblies should only be specified once."); + } + + AddApplicationServices(applicationServices, hostingServiceProvider); + + var host = new WebHost( + applicationServices, + hostingServiceProvider, + _options, + _config, + hostingStartupErrors); + try + { + host.Initialize(); + + return host; + } + catch + { + // Dispose the host if there's a failure to initialize, this should clean up + // will dispose services that were constructed until the exception was thrown + host.Dispose(); + throw; + } + + IServiceProvider GetProviderFromFactory(IServiceCollection collection) + { + var provider = collection.BuildServiceProvider(); + var factory = provider.GetService<IServiceProviderFactory<IServiceCollection>>(); + + if (factory != null) + { + using (provider) + { + return factory.CreateServiceProvider(factory.CreateBuilder(collection)); + } + } + + return provider; + } + } + + private IServiceCollection BuildCommonServices(out AggregateException hostingStartupErrors) + { + hostingStartupErrors = null; + + _options = new WebHostOptions(_config, Assembly.GetEntryAssembly()?.GetName().Name); + + if (!_options.PreventHostingStartup) + { + var exceptions = new List<Exception>(); + + // Execute the hosting startup assemblies + foreach (var assemblyName in _options.GetFinalHostingStartupAssemblies().Distinct(StringComparer.OrdinalIgnoreCase)) + { + try + { + var assembly = Assembly.Load(new AssemblyName(assemblyName)); + + foreach (var attribute in assembly.GetCustomAttributes<HostingStartupAttribute>()) + { + var hostingStartup = (IHostingStartup)Activator.CreateInstance(attribute.HostingStartupType); + hostingStartup.Configure(this); + } + } + catch (Exception ex) + { + // Capture any errors that happen during startup + exceptions.Add(new InvalidOperationException($"Startup assembly {assemblyName} failed to execute. See the inner exception for more details.", ex)); + } + } + + if (exceptions.Count > 0) + { + hostingStartupErrors = new AggregateException(exceptions); + } + } + + var contentRootPath = ResolveContentRootPath(_options.ContentRootPath, AppContext.BaseDirectory); + + // Initialize the hosting environment + _hostingEnvironment.Initialize(contentRootPath, _options); + _context.HostingEnvironment = _hostingEnvironment; + + var services = new ServiceCollection(); + services.AddSingleton(_options); + services.AddSingleton<IHostingEnvironment>(_hostingEnvironment); + services.AddSingleton<Extensions.Hosting.IHostingEnvironment>(_hostingEnvironment); + services.AddSingleton(_context); + + var builder = new ConfigurationBuilder() + .SetBasePath(_hostingEnvironment.ContentRootPath) + .AddConfiguration(_config); + + foreach (var configureAppConfiguration in _configureAppConfigurationBuilderDelegates) + { + configureAppConfiguration(_context, builder); + } + + var configuration = builder.Build(); + services.AddSingleton<IConfiguration>(configuration); + _context.Configuration = configuration; + + var listener = new DiagnosticListener("Microsoft.AspNetCore"); + services.AddSingleton<DiagnosticListener>(listener); + services.AddSingleton<DiagnosticSource>(listener); + + services.AddTransient<IApplicationBuilderFactory, ApplicationBuilderFactory>(); + services.AddTransient<IHttpContextFactory, HttpContextFactory>(); + services.AddScoped<IMiddlewareFactory, MiddlewareFactory>(); + services.AddOptions(); + services.AddLogging(); + + // Conjure up a RequestServices + services.AddTransient<IStartupFilter, AutoRequestServicesStartupFilter>(); + services.AddTransient<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>(); + + // Ensure object pooling is available everywhere. + services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>(); + + if (!string.IsNullOrEmpty(_options.StartupAssembly)) + { + try + { + var startupType = StartupLoader.FindStartupType(_options.StartupAssembly, _hostingEnvironment.EnvironmentName); + + if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo())) + { + services.AddSingleton(typeof(IStartup), startupType); + } + else + { + services.AddSingleton(typeof(IStartup), sp => + { + var hostingEnvironment = sp.GetRequiredService<IHostingEnvironment>(); + var methods = StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName); + return new ConventionBasedStartup(methods); + }); + } + } + catch (Exception ex) + { + var capture = ExceptionDispatchInfo.Capture(ex); + services.AddSingleton<IStartup>(_ => + { + capture.Throw(); + return null; + }); + } + } + + foreach (var configureServices in _configureServicesDelegates) + { + configureServices(_context, services); + } + + return services; + } + + private void AddApplicationServices(IServiceCollection services, IServiceProvider hostingServiceProvider) + { + // We are forwarding services from hosting container so hosting container + // can still manage their lifetime (disposal) shared instances with application services. + // NOTE: This code overrides original services lifetime. Instances would always be singleton in + // application container. + var listener = hostingServiceProvider.GetService<DiagnosticListener>(); + services.Replace(ServiceDescriptor.Singleton(typeof(DiagnosticListener), listener)); + services.Replace(ServiceDescriptor.Singleton(typeof(DiagnosticSource), listener)); + } + + private string ResolveContentRootPath(string contentRootPath, string basePath) + { + if (string.IsNullOrEmpty(contentRootPath)) + { + return basePath; + } + if (Path.IsPathRooted(contentRootPath)) + { + return contentRootPath; + } + return Path.Combine(Path.GetFullPath(basePath), contentRootPath); + } + } +} diff --git a/src/Hosting/Hosting/src/WebHostBuilderExtensions.cs b/src/Hosting/Hosting/src/WebHostBuilderExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..09c7e6d96b154bd8b0fcf8ad50117d4e450c68bf --- /dev/null +++ b/src/Hosting/Hosting/src/WebHostBuilderExtensions.cs @@ -0,0 +1,148 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Hosting +{ + public static class WebHostBuilderExtensions + { + /// <summary> + /// Specify the startup method to be used to configure the web application. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="configureApp">The delegate that configures the <see cref="IApplicationBuilder"/>.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder Configure(this IWebHostBuilder hostBuilder, Action<IApplicationBuilder> configureApp) + { + if (configureApp == null) + { + throw new ArgumentNullException(nameof(configureApp)); + } + + var startupAssemblyName = configureApp.GetMethodInfo().DeclaringType.GetTypeInfo().Assembly.GetName().Name; + + return hostBuilder + .UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName) + .ConfigureServices(services => + { + services.AddSingleton<IStartup>(sp => + { + return new DelegateStartup(sp.GetRequiredService<IServiceProviderFactory<IServiceCollection>>(), configureApp); + }); + }); + } + + + /// <summary> + /// Specify the startup type to be used by the web host. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="startupType">The <see cref="Type"/> to be used.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType) + { + var startupAssemblyName = startupType.GetTypeInfo().Assembly.GetName().Name; + + return hostBuilder + .UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName) + .ConfigureServices(services => + { + if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo())) + { + services.AddSingleton(typeof(IStartup), startupType); + } + else + { + services.AddSingleton(typeof(IStartup), sp => + { + var hostingEnvironment = sp.GetRequiredService<IHostingEnvironment>(); + return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName)); + }); + } + }); + } + + /// <summary> + /// Specify the startup type to be used by the web host. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <typeparam name ="TStartup">The type containing the startup methods for the application.</typeparam> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder UseStartup<TStartup>(this IWebHostBuilder hostBuilder) where TStartup : class + { + return hostBuilder.UseStartup(typeof(TStartup)); + } + + /// <summary> + /// Configures the default service provider + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="configure">A callback used to configure the <see cref="ServiceProviderOptions"/> for the default <see cref="IServiceProvider"/>.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder UseDefaultServiceProvider(this IWebHostBuilder hostBuilder, Action<ServiceProviderOptions> configure) + { + return hostBuilder.UseDefaultServiceProvider((context, options) => configure(options)); + } + + /// <summary> + /// Configures the default service provider + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="configure">A callback used to configure the <see cref="ServiceProviderOptions"/> for the default <see cref="IServiceProvider"/>.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder UseDefaultServiceProvider(this IWebHostBuilder hostBuilder, Action<WebHostBuilderContext, ServiceProviderOptions> configure) + { + return hostBuilder.ConfigureServices((context, services) => + { + var options = new ServiceProviderOptions(); + configure(context, options); + services.Replace(ServiceDescriptor.Singleton<IServiceProviderFactory<IServiceCollection>>(new DefaultServiceProviderFactory(options))); + }); + } + + /// <summary> + /// Adds a delegate for configuring the <see cref="IConfigurationBuilder"/> that will construct an <see cref="IConfiguration"/>. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> + /// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder" /> that will be used to construct an <see cref="IConfiguration" />.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + /// <remarks> + /// The <see cref="IConfiguration"/> and <see cref="ILoggerFactory"/> on the <see cref="WebHostBuilderContext"/> are uninitialized at this stage. + /// The <see cref="IConfigurationBuilder"/> is pre-populated with the settings of the <see cref="IWebHostBuilder"/>. + /// </remarks> + public static IWebHostBuilder ConfigureAppConfiguration(this IWebHostBuilder hostBuilder, Action<IConfigurationBuilder> configureDelegate) + { + return hostBuilder.ConfigureAppConfiguration((context, builder) => configureDelegate(builder)); + } + + /// <summary> + /// Adds a delegate for configuring the provided <see cref="ILoggingBuilder"/>. This may be called multiple times. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder" /> to configure.</param> + /// <param name="configureLogging">The delegate that configures the <see cref="ILoggingBuilder"/>.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder ConfigureLogging(this IWebHostBuilder hostBuilder, Action<ILoggingBuilder> configureLogging) + { + return hostBuilder.ConfigureServices(collection => collection.AddLogging(configureLogging)); + } + + /// <summary> + /// Adds a delegate for configuring the provided <see cref="LoggerFactory"/>. This may be called multiple times. + /// </summary> + /// <param name="hostBuilder">The <see cref="IWebHostBuilder" /> to configure.</param> + /// <param name="configureLogging">The delegate that configures the <see cref="LoggerFactory"/>.</param> + /// <returns>The <see cref="IWebHostBuilder"/>.</returns> + public static IWebHostBuilder ConfigureLogging(this IWebHostBuilder hostBuilder, Action<WebHostBuilderContext, ILoggingBuilder> configureLogging) + { + return hostBuilder.ConfigureServices((context, collection) => collection.AddLogging(builder => configureLogging(context, builder))); + } + } +} diff --git a/src/Hosting/Hosting/src/WebHostExtensions.cs b/src/Hosting/Hosting/src/WebHostExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..06a3e00cf887b6c4f44e452b9b6074452945a176 --- /dev/null +++ b/src/Hosting/Hosting/src/WebHostExtensions.cs @@ -0,0 +1,176 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Hosting.Internal; + +namespace Microsoft.AspNetCore.Hosting +{ + public static class WebHostExtensions + { + /// <summary> + /// Attempts to gracefully stop the host with the given timeout. + /// </summary> + /// <param name="host"></param> + /// <param name="timeout">The timeout for stopping gracefully. Once expired the + /// server may terminate any remaining active connections.</param> + /// <returns></returns> + public static Task StopAsync(this IWebHost host, TimeSpan timeout) + { + return host.StopAsync(new CancellationTokenSource(timeout).Token); + } + + /// <summary> + /// Block the calling thread until shutdown is triggered via Ctrl+C or SIGTERM. + /// </summary> + /// <param name="host">The running <see cref="IWebHost"/>.</param> + public static void WaitForShutdown(this IWebHost host) + { + host.WaitForShutdownAsync().GetAwaiter().GetResult(); + } + + /// <summary> + /// Returns a Task that completes when shutdown is triggered via the given token, Ctrl+C or SIGTERM. + /// </summary> + /// <param name="host">The running <see cref="IWebHost"/>.</param> + /// <param name="token">The token to trigger shutdown.</param> + public static async Task WaitForShutdownAsync(this IWebHost host, CancellationToken token = default) + { + var done = new ManualResetEventSlim(false); + using (var cts = CancellationTokenSource.CreateLinkedTokenSource(token)) + { + AttachCtrlcSigtermShutdown(cts, done, shutdownMessage: string.Empty); + + await host.WaitForTokenShutdownAsync(cts.Token); + done.Set(); + } + } + + /// <summary> + /// Runs a web application and block the calling thread until host shutdown. + /// </summary> + /// <param name="host">The <see cref="IWebHost"/> to run.</param> + public static void Run(this IWebHost host) + { + host.RunAsync().GetAwaiter().GetResult(); + } + + /// <summary> + /// Runs a web application and returns a Task that only completes when the token is triggered or shutdown is triggered. + /// </summary> + /// <param name="host">The <see cref="IWebHost"/> to run.</param> + /// <param name="token">The token to trigger shutdown.</param> + public static async Task RunAsync(this IWebHost host, CancellationToken token = default) + { + // Wait for token shutdown if it can be canceled + if (token.CanBeCanceled) + { + await host.RunAsync(token, shutdownMessage: null); + return; + } + + // If token cannot be canceled, attach Ctrl+C and SIGTERM shutdown + var done = new ManualResetEventSlim(false); + using (var cts = new CancellationTokenSource()) + { + var shutdownMessage = host.Services.GetRequiredService<WebHostOptions>().SuppressStatusMessages ? string.Empty : "Application is shutting down..."; + AttachCtrlcSigtermShutdown(cts, done, shutdownMessage: shutdownMessage); + + await host.RunAsync(cts.Token, "Application started. Press Ctrl+C to shut down."); + done.Set(); + } + } + + private static async Task RunAsync(this IWebHost host, CancellationToken token, string shutdownMessage) + { + using (host) + { + await host.StartAsync(token); + + var hostingEnvironment = host.Services.GetService<IHostingEnvironment>(); + var applicationLifetime = host.Services.GetService<IApplicationLifetime>(); + var options = host.Services.GetRequiredService<WebHostOptions>(); + + if (!options.SuppressStatusMessages) + { + Console.WriteLine($"Hosting environment: {hostingEnvironment.EnvironmentName}"); + Console.WriteLine($"Content root path: {hostingEnvironment.ContentRootPath}"); + + + var serverAddresses = host.ServerFeatures.Get<IServerAddressesFeature>()?.Addresses; + if (serverAddresses != null) + { + foreach (var address in serverAddresses) + { + Console.WriteLine($"Now listening on: {address}"); + } + } + + if (!string.IsNullOrEmpty(shutdownMessage)) + { + Console.WriteLine(shutdownMessage); + } + } + + await host.WaitForTokenShutdownAsync(token); + } + } + + private static void AttachCtrlcSigtermShutdown(CancellationTokenSource cts, ManualResetEventSlim resetEvent, string shutdownMessage) + { + void Shutdown() + { + if (!cts.IsCancellationRequested) + { + if (!string.IsNullOrEmpty(shutdownMessage)) + { + Console.WriteLine(shutdownMessage); + } + try + { + cts.Cancel(); + } + catch (ObjectDisposedException) { } + } + + // Wait on the given reset event + resetEvent.Wait(); + }; + + AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => Shutdown(); + Console.CancelKeyPress += (sender, eventArgs) => + { + Shutdown(); + // Don't terminate the process immediately, wait for the Main thread to exit gracefully. + eventArgs.Cancel = true; + }; + } + + private static async Task WaitForTokenShutdownAsync(this IWebHost host, CancellationToken token) + { + var applicationLifetime = host.Services.GetService<IApplicationLifetime>(); + + token.Register(state => + { + ((IApplicationLifetime)state).StopApplication(); + }, + applicationLifetime); + + var waitForStop = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); + applicationLifetime.ApplicationStopping.Register(obj => + { + var tcs = (TaskCompletionSource<object>)obj; + tcs.TrySetResult(null); + }, waitForStop); + + await waitForStop.Task; + + // WebHost will use its default ShutdownTimeout if none is specified. + await host.StopAsync(); + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/baseline.netcore.json b/src/Hosting/Hosting/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..ca859909149139c922bb272e8f0b55e011527d22 --- /dev/null +++ b/src/Hosting/Hosting/src/baseline.netcore.json @@ -0,0 +1,1995 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Hosting, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Hosting.ConventionBasedStartup", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Hosting.IStartup" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Configure", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IStartup", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ConfigureServices", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "System.IServiceProvider", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IStartup", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "methods", + "Type": "Microsoft.AspNetCore.Hosting.Internal.StartupMethods" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.DelegateStartup", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Hosting.StartupBase<Microsoft.Extensions.DependencyInjection.IServiceCollection>", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Configure", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IStartup", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "factory", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceProviderFactory<Microsoft.Extensions.DependencyInjection.IServiceCollection>" + }, + { + "Name": "configureApp", + "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.StartupBase", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Hosting.IStartup" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Configure", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IStartup", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ConfigureServices", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateServiceProvider", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "System.IServiceProvider", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.StartupBase<T0>", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Hosting.StartupBase", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CreateServiceProvider", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "System.IServiceProvider", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ConfigureContainer", + "Parameters": [ + { + "Name": "builder", + "Type": "T0" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "factory", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceProviderFactory<T0>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TBuilder", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.WebHostBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetSetting", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseSetting", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ConfigureServices", + "Parameters": [ + { + "Name": "configureServices", + "Type": "System.Action<Microsoft.Extensions.DependencyInjection.IServiceCollection>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ConfigureServices", + "Parameters": [ + { + "Name": "configureServices", + "Type": "System.Action<Microsoft.AspNetCore.Hosting.WebHostBuilderContext, Microsoft.Extensions.DependencyInjection.IServiceCollection>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ConfigureAppConfiguration", + "Parameters": [ + { + "Name": "configureDelegate", + "Type": "System.Action<Microsoft.AspNetCore.Hosting.WebHostBuilderContext, Microsoft.Extensions.Configuration.IConfigurationBuilder>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHost", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.WebHostBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Configure", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "configureApp", + "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseStartup", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "startupType", + "Type": "System.Type" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseStartup<T0>", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TStartup", + "ParameterPosition": 0, + "Class": true, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "UseDefaultServiceProvider", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "configure", + "Type": "System.Action<Microsoft.Extensions.DependencyInjection.ServiceProviderOptions>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseDefaultServiceProvider", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "configure", + "Type": "System.Action<Microsoft.AspNetCore.Hosting.WebHostBuilderContext, Microsoft.Extensions.DependencyInjection.ServiceProviderOptions>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ConfigureAppConfiguration", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "configureDelegate", + "Type": "System.Action<Microsoft.Extensions.Configuration.IConfigurationBuilder>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ConfigureLogging", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "configureLogging", + "Type": "System.Action<Microsoft.Extensions.Logging.ILoggingBuilder>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ConfigureLogging", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "configureLogging", + "Type": "System.Action<Microsoft.AspNetCore.Hosting.WebHostBuilderContext, Microsoft.Extensions.Logging.ILoggingBuilder>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.WebHostExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "StopAsync", + "Parameters": [ + { + "Name": "host", + "Type": "Microsoft.AspNetCore.Hosting.IWebHost" + }, + { + "Name": "timeout", + "Type": "System.TimeSpan" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WaitForShutdown", + "Parameters": [ + { + "Name": "host", + "Type": "Microsoft.AspNetCore.Hosting.IWebHost" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WaitForShutdownAsync", + "Parameters": [ + { + "Name": "host", + "Type": "Microsoft.AspNetCore.Hosting.IWebHost" + }, + { + "Name": "token", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "host", + "Type": "Microsoft.AspNetCore.Hosting.IWebHost" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RunAsync", + "Parameters": [ + { + "Name": "host", + "Type": "Microsoft.AspNetCore.Hosting.IWebHost" + }, + { + "Name": "token", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Server.Features.ServerAddressesFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Hosting.Server.Features.IServerAddressesFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Addresses", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection<System.String>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.Server.Features.IServerAddressesFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PreferHostingUrls", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.Server.Features.IServerAddressesFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PreferHostingUrls", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.Server.Features.IServerAddressesFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.ApplicationLifetime", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Hosting.IApplicationLifetime" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ApplicationStarted", + "Parameters": [], + "ReturnType": "System.Threading.CancellationToken", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IApplicationLifetime", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ApplicationStopping", + "Parameters": [], + "ReturnType": "System.Threading.CancellationToken", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IApplicationLifetime", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ApplicationStopped", + "Parameters": [], + "ReturnType": "System.Threading.CancellationToken", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IApplicationLifetime", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StopApplication", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IApplicationLifetime", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "NotifyStarted", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "NotifyStopped", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Hosting.Internal.ApplicationLifetime>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.AutoRequestServicesStartupFilter", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Hosting.IStartupFilter" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Configure", + "Parameters": [ + { + "Name": "next", + "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>" + } + ], + "ReturnType": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IStartupFilter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.ConfigureBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_MethodInfo", + "Parameters": [], + "ReturnType": "System.Reflection.MethodInfo", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [ + { + "Name": "instance", + "Type": "System.Object" + } + ], + "ReturnType": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "configure", + "Type": "System.Reflection.MethodInfo" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.ConfigureContainerBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_MethodInfo", + "Parameters": [], + "ReturnType": "System.Reflection.MethodInfo", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [ + { + "Name": "instance", + "Type": "System.Object" + } + ], + "ReturnType": "System.Action<System.Object>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetContainerType", + "Parameters": [], + "ReturnType": "System.Type", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "configureContainerMethod", + "Type": "System.Reflection.MethodInfo" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.ConfigureServicesBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_MethodInfo", + "Parameters": [], + "ReturnType": "System.Reflection.MethodInfo", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [ + { + "Name": "instance", + "Type": "System.Object" + } + ], + "ReturnType": "System.Func<Microsoft.Extensions.DependencyInjection.IServiceCollection, System.IServiceProvider>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "configureServices", + "Type": "System.Reflection.MethodInfo" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.HostedServiceExecutor", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "StartAsync", + "Parameters": [ + { + "Name": "token", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StopAsync", + "Parameters": [ + { + "Name": "token", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Hosting.Internal.HostedServiceExecutor>" + }, + { + "Name": "services", + "Type": "System.Collections.Generic.IEnumerable<Microsoft.Extensions.Hosting.IHostedService>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.HostingApplication", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Hosting.Server.IHttpApplication<Microsoft.AspNetCore.Hosting.Internal.HostingApplication+Context>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "CreateContext", + "Parameters": [ + { + "Name": "contextFeatures", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.Internal.HostingApplication+Context", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.Server.IHttpApplication<Microsoft.AspNetCore.Hosting.Internal.HostingApplication+Context>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ProcessRequestAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Hosting.Internal.HostingApplication+Context" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.Server.IHttpApplication<Microsoft.AspNetCore.Hosting.Internal.HostingApplication+Context>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DisposeContext", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Hosting.Internal.HostingApplication+Context" + }, + { + "Name": "exception", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.Server.IHttpApplication<Microsoft.AspNetCore.Hosting.Internal.HostingApplication+Context>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "application", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILogger" + }, + { + "Name": "diagnosticSource", + "Type": "System.Diagnostics.DiagnosticListener" + }, + { + "Name": "httpContextFactory", + "Type": "Microsoft.AspNetCore.Http.IHttpContextFactory" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.HostingEnvironment", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Hosting.IHostingEnvironment" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_EnvironmentName", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IHostingEnvironment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_EnvironmentName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IHostingEnvironment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ApplicationName", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IHostingEnvironment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ApplicationName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IHostingEnvironment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_WebRootPath", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IHostingEnvironment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_WebRootPath", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IHostingEnvironment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_WebRootFileProvider", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.FileProviders.IFileProvider", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IHostingEnvironment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_WebRootFileProvider", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.FileProviders.IFileProvider" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IHostingEnvironment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentRootPath", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IHostingEnvironment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentRootPath", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IHostingEnvironment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentRootFileProvider", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.FileProviders.IFileProvider", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IHostingEnvironment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentRootFileProvider", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.FileProviders.IFileProvider" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IHostingEnvironment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.HostingEnvironmentExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Initialize", + "Parameters": [ + { + "Name": "hostingEnvironment", + "Type": "Microsoft.AspNetCore.Hosting.IHostingEnvironment" + }, + { + "Name": "applicationName", + "Type": "System.String" + }, + { + "Name": "contentRootPath", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Hosting.Internal.WebHostOptions" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.HostingEventSource", + "Visibility": "Public", + "Kind": "Class", + "Sealed": true, + "BaseType": "System.Diagnostics.Tracing.EventSource", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "HostStart", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HostStop", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequestStart", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + }, + { + "Name": "path", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequestStop", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UnhandledException", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Log", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Hosting.Internal.HostingEventSource", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.RequestServicesContainerMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "scopeFactory", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceScopeFactory" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.RequestServicesFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IServiceProvidersFeature", + "System.IDisposable" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequestServices", + "Parameters": [], + "ReturnType": "System.IServiceProvider", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IServiceProvidersFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestServices", + "Parameters": [ + { + "Name": "value", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IServiceProvidersFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "scopeFactory", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceScopeFactory" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.StartupLoader", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "LoadMethods", + "Parameters": [ + { + "Name": "hostingServiceProvider", + "Type": "System.IServiceProvider" + }, + { + "Name": "startupType", + "Type": "System.Type" + }, + { + "Name": "environmentName", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.Internal.StartupMethods", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FindStartupType", + "Parameters": [ + { + "Name": "startupAssemblyName", + "Type": "System.String" + }, + { + "Name": "environmentName", + "Type": "System.String" + } + ], + "ReturnType": "System.Type", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.StartupMethods", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_StartupInstance", + "Parameters": [], + "ReturnType": "System.Object", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConfigureServicesDelegate", + "Parameters": [], + "ReturnType": "System.Func<Microsoft.Extensions.DependencyInjection.IServiceCollection, System.IServiceProvider>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConfigureDelegate", + "Parameters": [], + "ReturnType": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "instance", + "Type": "System.Object" + }, + { + "Name": "configure", + "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>" + }, + { + "Name": "configureServices", + "Type": "System.Func<Microsoft.Extensions.DependencyInjection.IServiceCollection, System.IServiceProvider>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.WebHostOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ApplicationName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ApplicationName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PreventHostingStartup", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PreventHostingStartup", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HostingStartupAssemblies", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IReadOnlyList<System.String>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HostingStartupAssemblies", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IReadOnlyList<System.String>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DetailedErrors", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DetailedErrors", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CaptureStartupErrors", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CaptureStartupErrors", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Environment", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Environment", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StartupAssembly", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StartupAssembly", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_WebRoot", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_WebRoot", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentRootPath", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentRootPath", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ShutdownTimeout", + "Parameters": [], + "ReturnType": "System.TimeSpan", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ShutdownTimeout", + "Parameters": [ + { + "Name": "value", + "Type": "System.TimeSpan" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "configuration", + "Type": "Microsoft.Extensions.Configuration.IConfiguration" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.WebHostUtilities", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "ParseBool", + "Parameters": [ + { + "Name": "configuration", + "Type": "Microsoft.Extensions.Configuration.IConfiguration" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Builder.ApplicationBuilderFactory", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory" + ], + "Members": [ + { + "Kind": "Method", + "Name": "CreateBuilder", + "Parameters": [ + { + "Name": "serverFeatures", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "serviceProvider", + "Type": "System.IServiceProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CreateBuilder", + "Parameters": [ + { + "Name": "serverFeatures", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Internal.HostingApplication+Context", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HttpContext", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Scope", + "Parameters": [], + "ReturnType": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Scope", + "Parameters": [ + { + "Name": "value", + "Type": "System.IDisposable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StartTimestamp", + "Parameters": [], + "ReturnType": "System.Int64", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StartTimestamp", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_EventLogEnabled", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_EventLogEnabled", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Activity", + "Parameters": [], + "ReturnType": "System.Diagnostics.Activity", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Activity", + "Parameters": [ + { + "Name": "value", + "Type": "System.Diagnostics.Activity" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Hosting/Hosting/src/compiler/resources/GenericError.html b/src/Hosting/Hosting/src/compiler/resources/GenericError.html new file mode 100644 index 0000000000000000000000000000000000000000..c6b24c57e852e2294387e2da2429b948c6c8d48b --- /dev/null +++ b/src/Hosting/Hosting/src/compiler/resources/GenericError.html @@ -0,0 +1,146 @@ +<!DOCTYPE html> + +<html> +<head> + <meta charset="utf-8" /> + <title>500 Internal Server Error</title> + <style type="text/css"> + body { + background-color: white; + color: #111111; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + margin: 2em 4em; + } + + footer a { + color: darkblue; + text-decoration: none; + font-weight: bolder; + } + + #header { + margin-bottom: 2.5em; + } + + .stacktrace pre { + display: inline; + } + + .faded { + color: #999999; + font-weight: normal; + } + + div.message { + margin-top: 2.5em; + padding: 0.3em 1em; + border-left: 0.25em solid red; + } + + .light { + font-size: 1.3em; + font-weight: lighter; + } + + .heavy { + font-size: 1.5em; + } + + .exception { + color: red; + } + + .stacktrace { + padding-top: 0.3em; + padding-left: 2em; + display: block; + font-weight: bold; + } + + .codeSnippet { + margin-left: 2em; + margin-top: 1em; + margin-bottom: 1em; + display: inline-block; + border-top: 0.2em solid #cccccc; + border-bottom: 0.2em solid #cccccc; + color: black; + } + + .codeSnippet div:nth-of-type(2n) { + background-color: #f0f0f0; + } + + .codeSnippet div:nth-of-type(2n + 1) { + background-color: #f6f6f6; + } + + .codeSnippet div.filename { + font-weight: bold; + background-color: white; + margin: 0.6em; + } + + .codeSnippet div.line { + padding: 0.2em; + line-height: 1em; + } + + .codeSnippet div.line .line-number { + color: #999999; + text-align: right; + margin-right: 0.5em; + } + + .codeSnippet div.error { + color: red; + font-weight: bolder; + background-color: #ffeda7; + } + + .codeSnippet code { + white-space: pre; + } + + .rawExceptionBlock { + margin-top: 1em; + margin-left: 1em; + } + + #rawException { + display: none; + } + + footer { + margin-top: 2em; + font-size: smaller; + font-weight: lighter; + } + </style> + <script type="text/javascript"> + function showRawException() { + var div = document.getElementById('rawException'); + div.style.display = 'inline-block'; + div.scrollIntoView(true); + } + </script> +</head> +<body> + + <div id="header"> + <div style="font-size: 6em; display: inline-block;"> + :( + </div> + <div style="display: inline-block; padding-left: 3em;"> + <span style="font-size: 2em;">Oops.</span><br /> + <span style="font-size: 1.65em; font-weight: lighter;">500 Internal Server Error</span> + </div> + </div> + + [[[0]]] + + [[[1]]] + + [[[2]]] +</body> +</html> diff --git a/src/Hosting/Hosting/src/compiler/resources/GenericError_Exception.html b/src/Hosting/Hosting/src/compiler/resources/GenericError_Exception.html new file mode 100644 index 0000000000000000000000000000000000000000..012e05bee7dfdf4f32ad1371d80df985f522e559 --- /dev/null +++ b/src/Hosting/Hosting/src/compiler/resources/GenericError_Exception.html @@ -0,0 +1,8 @@ +<div class="message"> + <span class="light exception">{0}</span><br /> + <span class="heavy">{1}</span><br /> + {2} + <div class="stacktrace"> + {3} + </div> +</div> diff --git a/src/Hosting/Hosting/src/compiler/resources/GenericError_Footer.html b/src/Hosting/Hosting/src/compiler/resources/GenericError_Footer.html new file mode 100644 index 0000000000000000000000000000000000000000..fe54861d87771f8078ab264b1f9ebd4e0b517c35 --- /dev/null +++ b/src/Hosting/Hosting/src/compiler/resources/GenericError_Footer.html @@ -0,0 +1,3 @@ +<footer> + {0} {1} v{2} | Microsoft.AspNetCore.Hosting version {3} | {4} | <a href="http://go.microsoft.com/fwlink/?LinkId=517394">Need help?</a> +</footer> diff --git a/src/Hosting/Hosting/src/compiler/resources/GenericError_Message.html b/src/Hosting/Hosting/src/compiler/resources/GenericError_Message.html new file mode 100644 index 0000000000000000000000000000000000000000..39a83d8754cdfdace8d6e046e371053b62eec115 --- /dev/null +++ b/src/Hosting/Hosting/src/compiler/resources/GenericError_Message.html @@ -0,0 +1,3 @@ +<div class="message"> + <span class="heavy">{0}</span><br /> +</div> diff --git a/src/Hosting/Hosting/test/ConfigureBuilderTests.cs b/src/Hosting/Hosting/test/ConfigureBuilderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..cea4235c8c7edef0c8ec6fe0227c5988175a38b2 --- /dev/null +++ b/src/Hosting/Hosting/test/ConfigureBuilderTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Hosting.Tests +{ + public class ConfigureBuilderTests + { + [Fact] + public void CapturesServiceExceptionDetails() + { + var methodInfo = GetType().GetMethod(nameof(InjectedMethod), BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(methodInfo); + + var services = new ServiceCollection() + .AddSingleton<CrasherService>() + .BuildServiceProvider(); + + var applicationBuilder = new ApplicationBuilder(services); + + var builder = new ConfigureBuilder(methodInfo); + Action<IApplicationBuilder> action = builder.Build(instance:null); + var ex = Assert.Throws<Exception>(() => action.Invoke(applicationBuilder)); + + Assert.NotNull(ex); + Assert.Equal($"Could not resolve a service of type '{typeof(CrasherService).FullName}' for the parameter" + + $" 'service' of method '{methodInfo.Name}' on type '{methodInfo.DeclaringType.FullName}'.", ex.Message); + + // the inner exception contains the root cause + Assert.NotNull(ex.InnerException); + Assert.Equal("Service instantiation failed", ex.InnerException.Message); + Assert.Contains(nameof(CrasherService), ex.InnerException.StackTrace); + } + + private static void InjectedMethod(CrasherService service) + { + Assert.NotNull(service); + } + + private class CrasherService + { + public CrasherService() + { + throw new Exception("Service instantiation failed"); + } + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/CustomLoggerFactory.cs b/src/Hosting/Hosting/test/Fakes/CustomLoggerFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..9fa7cf2151ee80e279195afd9eb0a2d9b532935a --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/CustomLoggerFactory.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class CustomLoggerFactory : ILoggerFactory + { + public void CustomConfigureMethod() { } + + public void AddProvider(ILoggerProvider provider) { } + + public ILogger CreateLogger(string categoryName) => NullLogger.Instance; + + public void Dispose() { } + } + + public class SubLoggerFactory : CustomLoggerFactory { } + + public class NonSubLoggerFactory : ILoggerFactory + { + public void CustomConfigureMethod() { } + + public void AddProvider(ILoggerProvider provider) { } + + public ILogger CreateLogger(string categoryName) => NullLogger.Instance; + + public void Dispose() { } + } +} diff --git a/src/Hosting/Hosting/test/Fakes/FakeOptions.cs b/src/Hosting/Hosting/test/Fakes/FakeOptions.cs new file mode 100644 index 0000000000000000000000000000000000000000..c4ffaa799d80f9f3c55ddc833bc4c1d825c3058f --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/FakeOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class FakeOptions + { + public bool Configured { get; set; } + public string Environment { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/FakeService.cs b/src/Hosting/Hosting/test/Fakes/FakeService.cs new file mode 100644 index 0000000000000000000000000000000000000000..3bf3e6ce38ba657b362e6d85f88f7d43be7f4406 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/FakeService.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class FakeService : IFakeEveryService, IDisposable + { + public bool Disposed { get; private set; } + + public void Dispose() + { + Disposed = true; + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/IFactoryService.cs b/src/Hosting/Hosting/test/Fakes/IFactoryService.cs new file mode 100644 index 0000000000000000000000000000000000000000..5b78e046e16e10196ce3fce39234541c0e9143ff --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/IFactoryService.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public interface IFactoryService + { + IFakeService FakeService { get; } + + int Value { get; } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/IFakeEveryService.cs b/src/Hosting/Hosting/test/Fakes/IFakeEveryService.cs new file mode 100644 index 0000000000000000000000000000000000000000..2cc7a00701be90bf50aa494212cec07874a276c2 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/IFakeEveryService.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + interface IFakeEveryService : + IFakeScopedService, + IFakeServiceInstance, + IFakeSingletonService + { + } +} diff --git a/src/Hosting/Hosting/test/Fakes/IFakeScopedService.cs b/src/Hosting/Hosting/test/Fakes/IFakeScopedService.cs new file mode 100644 index 0000000000000000000000000000000000000000..77c53e596b401b197b42a3c6f09f56a55050dd1a --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/IFakeScopedService.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public interface IFakeScopedService : IFakeService + { + } +} diff --git a/src/Hosting/Hosting/test/Fakes/IFakeService.cs b/src/Hosting/Hosting/test/Fakes/IFakeService.cs new file mode 100644 index 0000000000000000000000000000000000000000..73fca3bab1bf2752113b6b74d88eb7a5e4706983 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/IFakeService.cs @@ -0,0 +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. + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public interface IFakeService { } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/IFakeServiceInstance.cs b/src/Hosting/Hosting/test/Fakes/IFakeServiceInstance.cs new file mode 100644 index 0000000000000000000000000000000000000000..0225a6789fd73eec8742e52056d6b064fddb3212 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/IFakeServiceInstance.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + interface IFakeServiceInstance : IFakeService + { + } +} diff --git a/src/Hosting/Hosting/test/Fakes/IFakeSingletonService.cs b/src/Hosting/Hosting/test/Fakes/IFakeSingletonService.cs new file mode 100644 index 0000000000000000000000000000000000000000..93873059994617ede184ed8282f8c2c017c457ec --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/IFakeSingletonService.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + interface IFakeSingletonService : IFakeService + { + } +} diff --git a/src/Hosting/Hosting/test/Fakes/IFakeStartupCallback.cs b/src/Hosting/Hosting/test/Fakes/IFakeStartupCallback.cs new file mode 100644 index 0000000000000000000000000000000000000000..8e345a1020d71cda8cdb33e3c5b948ff93d54848 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/IFakeStartupCallback.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public interface IFakeStartupCallback + { + void ConfigurationMethodCalled(object instance); + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/INonexistentService.cs b/src/Hosting/Hosting/test/Fakes/INonexistentService.cs new file mode 100644 index 0000000000000000000000000000000000000000..5090051fd6ecac825db36a06fc78471c3d0db6c9 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/INonexistentService.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public interface INonexistentService + { + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/Startup.cs b/src/Hosting/Hosting/test/Fakes/Startup.cs new file mode 100644 index 0000000000000000000000000000000000000000..2abe4a4b223d2e4402035049eb66cb5a64476e68 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/Startup.cs @@ -0,0 +1,99 @@ +// 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.Builder; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class Startup : StartupBase + { + public Startup() + { + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddOptions(); + services.Configure<FakeOptions>(o => o.Configured = true); + } + + public void ConfigureDevServices(IServiceCollection services) + { + services.AddOptions(); + services.Configure<FakeOptions>(o => + { + o.Configured = true; + o.Environment = "Dev"; + }); + } + + public void ConfigureRetailServices(IServiceCollection services) + { + services.AddOptions(); + services.Configure<FakeOptions>(o => + { + o.Configured = true; + o.Environment = "Retail"; + }); + } + + public static void ConfigureStaticServices(IServiceCollection services) + { + services.AddOptions(); + services.Configure<FakeOptions>(o => + { + o.Configured = true; + o.Environment = "Static"; + }); + } + + public static IServiceProvider ConfigureStaticProviderServices() + { + var services = new ServiceCollection().AddOptions(); + services.Configure<FakeOptions>(o => + { + o.Configured = true; + o.Environment = "StaticProvider"; + }); + return services.BuildServiceProvider(); + } + + public static IServiceProvider ConfigureFallbackProviderServices(IServiceProvider fallback) + { + return fallback; + } + + public static IServiceProvider ConfigureNullServices() + { + return null; + } + + public IServiceProvider ConfigureProviderServices(IServiceCollection services) + { + services.AddOptions(); + services.Configure<FakeOptions>(o => + { + o.Configured = true; + o.Environment = "Provider"; + }); + return services.BuildServiceProvider(); + } + + public IServiceProvider ConfigureProviderArgsServices() + { + var services = new ServiceCollection().AddOptions(); + services.Configure<FakeOptions>(o => + { + o.Configured = true; + o.Environment = "ProviderArgs"; + }); + return services.BuildServiceProvider(); + } + + public virtual void Configure(IApplicationBuilder builder) + { + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupBase.cs b/src/Hosting/Hosting/test/Fakes/StartupBase.cs new file mode 100644 index 0000000000000000000000000000000000000000..82dd2c7cb6e2e080ac57479b5d5c44eca47b5096 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupBase.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupBase + { + public void ConfigureBaseClassServices(IServiceCollection services) + { + services.AddOptions(); + services.Configure<FakeOptions>(o => + { + o.Configured = true; + o.Environment = "BaseClass"; + }); + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupBoom.cs b/src/Hosting/Hosting/test/Fakes/StartupBoom.cs new file mode 100644 index 0000000000000000000000000000000000000000..2b629896bc54ee76efde3c8817ab69bda77d1b66 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupBoom.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupBoom + { + public StartupBoom() + { + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupCaseInsensitive.cs b/src/Hosting/Hosting/test/Fakes/StartupCaseInsensitive.cs new file mode 100644 index 0000000000000000000000000000000000000000..0c85ad54133c7b2302e9e85b7905f0f7ad0e9690 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupCaseInsensitive.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Fakes; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Tests.Fakes +{ + class StartupCaseInsensitive + { + public static IServiceProvider ConfigureCaseInsensitiveServices(IServiceCollection services) + { + services.AddOptions(); + services.Configure<FakeOptions>(o => + { + o.Configured = true; + o.Environment = "ConfigureCaseInsensitiveServices"; + }); + return services.BuildServiceProvider(); + } + + public void ConfigureCaseInsensitive(IApplicationBuilder app) + { + } + } +} diff --git a/src/Hosting/Hosting/test/Fakes/StartupConfigureServicesThrows.cs b/src/Hosting/Hosting/test/Fakes/StartupConfigureServicesThrows.cs new file mode 100644 index 0000000000000000000000000000000000000000..895fa654e9e371d86da31296024f6396b08aec28 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupConfigureServicesThrows.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupConfigureServicesThrows + { + public void ConfigureServices(IServiceCollection services) + { + throw new Exception("Exception from ConfigureServices"); + } + + public void Configure(IApplicationBuilder builder) + { + + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupConfigureThrows.cs b/src/Hosting/Hosting/test/Fakes/StartupConfigureThrows.cs new file mode 100644 index 0000000000000000000000000000000000000000..1d9fa8ef37b3523a777b157f5833743321fafafa --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupConfigureThrows.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupConfigureThrows + { + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder builder) + { + throw new Exception("Exception from Configure"); + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupCtorThrows.cs b/src/Hosting/Hosting/test/Fakes/StartupCtorThrows.cs new file mode 100644 index 0000000000000000000000000000000000000000..b7c1f223d323343690fa2d3277da3e10f480b4ef --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupCtorThrows.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupCtorThrows + { + public StartupCtorThrows() + { + throw new Exception("Exception from constructor"); + } + + public void Configure(IApplicationBuilder app) + { + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupNoServices.cs b/src/Hosting/Hosting/test/Fakes/StartupNoServices.cs new file mode 100644 index 0000000000000000000000000000000000000000..93e054fbc6e1a804962daaaafe975a3834ca059a --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupNoServices.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupNoServices : Hosting.StartupBase + { + public StartupNoServices() + { + } + + public override void Configure(IApplicationBuilder builder) + { + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupPrivateConfigure.cs b/src/Hosting/Hosting/test/Fakes/StartupPrivateConfigure.cs new file mode 100644 index 0000000000000000000000000000000000000000..e421ba08c73911bc660ec7fee8780a30f468a871 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupPrivateConfigure.cs @@ -0,0 +1,25 @@ +// 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.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupPrivateConfigure + { + public StartupPrivateConfigure() + { + } + + public void ConfigureServices(IServiceCollection services) + { + + } + + private void Configure(IApplicationBuilder builder) + { + + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupStaticCtorThrows.cs b/src/Hosting/Hosting/test/Fakes/StartupStaticCtorThrows.cs new file mode 100644 index 0000000000000000000000000000000000000000..c9164fa98ff732982e7f554e76536baca6d55474 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupStaticCtorThrows.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupStaticCtorThrows + { + static StartupStaticCtorThrows() + { + throw new Exception("Exception from static constructor"); + } + + public void Configure(IApplicationBuilder app) + { + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupThrowTypeLoadException.cs b/src/Hosting/Hosting/test/Fakes/StartupThrowTypeLoadException.cs new file mode 100644 index 0000000000000000000000000000000000000000..b3cbd602259cb148608b080b6ccef0f6aa69a149 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupThrowTypeLoadException.cs @@ -0,0 +1,25 @@ +// 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.Reflection; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupThrowTypeLoadException + { + public StartupThrowTypeLoadException() + { + // For this exception, the error page should contain details of the LoaderExceptions + throw new ReflectionTypeLoadException( + classes: new Type[] { GetType() }, + exceptions: new Exception[] { new FileNotFoundException("Message from the LoaderException") }, + message: "This should not be in the output"); + } + + public void Configure() + { + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupTwoConfigureServices.cs b/src/Hosting/Hosting/test/Fakes/StartupTwoConfigureServices.cs new file mode 100644 index 0000000000000000000000000000000000000000..e7c1be78f927af489757b6ff1d657b76c0e6a54e --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupTwoConfigureServices.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupTwoConfigureServices + { + public StartupTwoConfigureServices() + { + } + + public void ConfigureServices(IServiceCollection services) + { + + } + + public void ConfigureServices(IServiceCollection services, object service) + { + + } + + public void Configure(IApplicationBuilder builder) + { + + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupTwoConfigures.cs b/src/Hosting/Hosting/test/Fakes/StartupTwoConfigures.cs new file mode 100644 index 0000000000000000000000000000000000000000..ce4132ac136614eb020c7abb4bf013de997a492c --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupTwoConfigures.cs @@ -0,0 +1,24 @@ +// 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.Builder; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupTwoConfigures + { + public StartupTwoConfigures() + { + } + + public void Configure(IApplicationBuilder builder) + { + + } + + public void Configure(IApplicationBuilder builder, object service) + { + + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupWithConfigureServices.cs b/src/Hosting/Hosting/test/Fakes/StartupWithConfigureServices.cs new file mode 100644 index 0000000000000000000000000000000000000000..4c397fe8bdd6b4419cf073b5521f1191c230ca93 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupWithConfigureServices.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupWithConfigureServices + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton<IFoo, Foo>(); + } + + public void Configure(IApplicationBuilder app, IFoo foo) + { + foo.Bar(); + } + + public interface IFoo + { + bool Invoked { get; } + void Bar(); + } + + public class Foo : IFoo + { + public bool Invoked { get; private set; } + + public void Bar() + { + Invoked = true; + } + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupWithConfigureServicesNotResolved.cs b/src/Hosting/Hosting/test/Fakes/StartupWithConfigureServicesNotResolved.cs new file mode 100644 index 0000000000000000000000000000000000000000..bff10f94421b97e607cec41e1f9af7218c07f5b8 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupWithConfigureServicesNotResolved.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupWithConfigureServicesNotResolved + { + public StartupWithConfigureServicesNotResolved() + { + } + + public void Configure(IApplicationBuilder builder, int notAService) + { + } + } +} diff --git a/src/Hosting/Hosting/test/Fakes/StartupWithHostingEnvironment.cs b/src/Hosting/Hosting/test/Fakes/StartupWithHostingEnvironment.cs new file mode 100644 index 0000000000000000000000000000000000000000..c4a57765028d57fb44ed85a22ccf96452553b159 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupWithHostingEnvironment.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Hosting.Tests.Fakes +{ + public class StartupWithHostingEnvironment + { + public StartupWithHostingEnvironment(IHostingEnvironment env) + { + env.EnvironmentName = "Changed"; + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupWithILoggerFactory.cs b/src/Hosting/Hosting/test/Fakes/StartupWithILoggerFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..ad596e2546169c1e81e5a48f8b3a19d04cbe5f67 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupWithILoggerFactory.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupWithILoggerFactory + { + public ILoggerFactory ConstructorLoggerFactory { get; set; } + + public ILoggerFactory ConfigureLoggerFactory { get; set; } + + public StartupWithILoggerFactory(ILoggerFactory constructorLoggerFactory) + { + ConstructorLoggerFactory = constructorLoggerFactory; + } + + public void ConfigureServices(IServiceCollection collection) + { + collection.AddSingleton(this); + } + + public void Configure(IApplicationBuilder builder, ILoggerFactory loggerFactory) + { + ConfigureLoggerFactory = loggerFactory; + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupWithNullConfigureServices.cs b/src/Hosting/Hosting/test/Fakes/StartupWithNullConfigureServices.cs new file mode 100644 index 0000000000000000000000000000000000000000..9390f4727fa4f86257a72019199125d03cacddb1 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupWithNullConfigureServices.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupWithNullConfigureServices + { + public IServiceProvider ConfigureServices(IServiceCollection services) + { + return null; + } + + public void Configure(IApplicationBuilder app) { } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/Fakes/StartupWithScopedServices.cs b/src/Hosting/Hosting/test/Fakes/StartupWithScopedServices.cs new file mode 100644 index 0000000000000000000000000000000000000000..985f920473fe3b15a7abb08d6c23acf58c438410 --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupWithScopedServices.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Builder; +using static Microsoft.AspNetCore.Hosting.Tests.StartupManagerTests; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupWithScopedServices + { + public DisposableService DisposableService { get; set; } + + public void Configure(IApplicationBuilder builder, DisposableService disposable) + { + DisposableService = disposable; + } + } +} diff --git a/src/Hosting/Hosting/test/Fakes/StartupWithServices.cs b/src/Hosting/Hosting/test/Fakes/StartupWithServices.cs new file mode 100644 index 0000000000000000000000000000000000000000..7056b37d687f43549dd0b87b3bd0510bdd69c31e --- /dev/null +++ b/src/Hosting/Hosting/test/Fakes/StartupWithServices.cs @@ -0,0 +1,23 @@ +// 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.Builder; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupWithServices + { + private readonly IFakeStartupCallback _fakeStartupCallback; + + public StartupWithServices(IFakeStartupCallback fakeStartupCallback) + { + _fakeStartupCallback = fakeStartupCallback; + } + + public void Configure(IApplicationBuilder builder, IFakeStartupCallback fakeStartupCallback2) + { + _fakeStartupCallback.ConfigurationMethodCalled(this); + fakeStartupCallback2.ConfigurationMethodCalled(this); + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/HostingApplicationTests.cs b/src/Hosting/Hosting/test/HostingApplicationTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..de1dc01899f9ef61fa568c1ee8f0ff4b286fe89e --- /dev/null +++ b/src/Hosting/Hosting/test/HostingApplicationTests.cs @@ -0,0 +1,371 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Hosting.Tests +{ + public class HostingApplicationTests + { + [Fact] + public void DisposeContextDoesNotThrowWhenContextScopeIsNull() + { + // Arrange + var hostingApplication = CreateApplication(out var features); + var context = hostingApplication.CreateContext(features); + + // Act/Assert + hostingApplication.DisposeContext(context, null); + } + + [Fact] + public void CreateContextSetsCorrelationIdInScope() + { + // Arrange + var logger = new LoggerWithScopes(); + var hostingApplication = CreateApplication(out var features, logger: logger); + features.Get<IHttpRequestFeature>().Headers["Request-Id"] = "some correlation id"; + + // Act + var context = hostingApplication.CreateContext(features); + + Assert.Single(logger.Scopes); + var pairs = ((IReadOnlyList<KeyValuePair<string, object>>)logger.Scopes[0]).ToDictionary(p => p.Key, p => p.Value); + Assert.Equal("some correlation id", pairs["CorrelationId"].ToString()); + } + + [Fact] + public void ActivityIsNotCreatedWhenIsEnabledForActivityIsFalse() + { + var diagnosticSource = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); + + bool eventsFired = false; + bool isEnabledActivityFired = false; + bool isEnabledStartFired = false; + + diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => + { + eventsFired |= pair.Key.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn"); + }), (s, o, arg3) => + { + if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn") + { + Assert.IsAssignableFrom<HttpContext>(o); + isEnabledActivityFired = true; + } + if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") + { + isEnabledStartFired = true; + } + return false; + }); + + hostingApplication.CreateContext(features); + Assert.Null(Activity.Current); + Assert.True(isEnabledActivityFired); + Assert.False(isEnabledStartFired); + Assert.False(eventsFired); + } + + [Fact] + public void ActivityIsCreatedButNotLoggedWhenIsEnabledForActivityStartIsFalse() + { + var diagnosticSource = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); + + bool eventsFired = false; + bool isEnabledStartFired = false; + bool isEnabledActivityFired = false; + + diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => + { + eventsFired |= pair.Key.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn"); + }), (s, o, arg3) => + { + if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn") + { + Assert.IsAssignableFrom<HttpContext>(o); + isEnabledActivityFired = true; + return true; + } + + if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") + { + isEnabledStartFired = true; + return false; + } + return true; + }); + + hostingApplication.CreateContext(features); + Assert.NotNull(Activity.Current); + Assert.True(isEnabledActivityFired); + Assert.True(isEnabledStartFired); + Assert.False(eventsFired); + } + + [Fact] + public void ActivityIsCreatedAndLogged() + { + var diagnosticSource = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); + + bool startCalled = false; + + diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => + { + if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") + { + startCalled = true; + Assert.NotNull(pair.Value); + Assert.NotNull(Activity.Current); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); + AssertProperty<HttpContext>(pair.Value, "HttpContext"); + } + })); + + hostingApplication.CreateContext(features); + Assert.NotNull(Activity.Current); + Assert.True(startCalled); + } + + [Fact] + public void ActivityIsStoppedDuringStopCall() + { + var diagnosticSource = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); + + bool endCalled = false; + diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => + { + if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop") + { + endCalled = true; + + Assert.NotNull(Activity.Current); + Assert.True(Activity.Current.Duration > TimeSpan.Zero); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); + AssertProperty<HttpContext>(pair.Value, "HttpContext"); + } + })); + + var context = hostingApplication.CreateContext(features); + hostingApplication.DisposeContext(context, null); + Assert.True(endCalled); + } + + [Fact] + public void ActivityIsStoppedDuringUnhandledExceptionCall() + { + var diagnosticSource = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); + + bool endCalled = false; + diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => + { + if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop") + { + endCalled = true; + Assert.NotNull(Activity.Current); + Assert.True(Activity.Current.Duration > TimeSpan.Zero); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); + AssertProperty<HttpContext>(pair.Value, "HttpContext"); + } + })); + + var context = hostingApplication.CreateContext(features); + hostingApplication.DisposeContext(context, new Exception()); + Assert.True(endCalled); + } + + [Fact] + public void ActivityIsAvailableDuringUnhandledExceptionCall() + { + var diagnosticSource = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); + + bool endCalled = false; + diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => + { + if (pair.Key == "Microsoft.AspNetCore.Hosting.UnhandledException") + { + endCalled = true; + Assert.NotNull(Activity.Current); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); + } + })); + + var context = hostingApplication.CreateContext(features); + hostingApplication.DisposeContext(context, new Exception()); + Assert.True(endCalled); + } + + [Fact] + public void ActivityIsAvailibleDuringRequest() + { + var diagnosticSource = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); + + diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => { }), + s => + { + if (s.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn")) + { + return true; + } + return false; + }); + + hostingApplication.CreateContext(features); + + Assert.NotNull(Activity.Current); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); + } + + [Fact] + public void ActivityParentIdAndBaggeReadFromHeaders() + { + var diagnosticSource = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); + + diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => { }), + s => + { + if (s.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn")) + { + return true; + } + return false; + }); + + features.Set<IHttpRequestFeature>(new HttpRequestFeature() + { + Headers = new HeaderDictionary() + { + {"Request-Id", "ParentId1"}, + {"Correlation-Context", "Key1=value1, Key2=value2"} + } + }); + hostingApplication.CreateContext(features); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); + Assert.Equal("ParentId1", Activity.Current.ParentId); + Assert.Contains(Activity.Current.Baggage, pair => pair.Key == "Key1" && pair.Value == "value1"); + Assert.Contains(Activity.Current.Baggage, pair => pair.Key == "Key2" && pair.Value == "value2"); + } + + private static void AssertProperty<T>(object o, string name) + { + Assert.NotNull(o); + var property = o.GetType().GetTypeInfo().GetProperty(name, BindingFlags.Instance | BindingFlags.Public); + Assert.NotNull(property); + var value = property.GetValue(o); + Assert.NotNull(value); + Assert.IsAssignableFrom<T>(value); + } + + private static HostingApplication CreateApplication(out FeatureCollection features, + DiagnosticListener diagnosticSource = null, ILogger logger = null) + { + var httpContextFactory = new Mock<IHttpContextFactory>(); + + features = new FeatureCollection(); + features.Set<IHttpRequestFeature>(new HttpRequestFeature()); + httpContextFactory.Setup(s => s.Create(It.IsAny<IFeatureCollection>())).Returns(new DefaultHttpContext(features)); + httpContextFactory.Setup(s => s.Dispose(It.IsAny<HttpContext>())); + + var hostingApplication = new HostingApplication( + ctx => Task.FromResult(0), + logger ?? new NullScopeLogger(), + diagnosticSource ?? new NoopDiagnosticSource(), + httpContextFactory.Object); + + return hostingApplication; + } + + private class NullScopeLogger : ILogger + { + public IDisposable BeginScope<TState>(TState state) => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) + { + } + } + + private class LoggerWithScopes : ILogger + { + public IDisposable BeginScope<TState>(TState state) + { + Scopes.Add(state); + return new Scope(); + } + + public List<object> Scopes { get; set; } = new List<object>(); + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) + { + + } + + private class Scope : IDisposable + { + public void Dispose() + { + } + } + } + + private class NoopDiagnosticSource : DiagnosticListener + { + public NoopDiagnosticSource() : base("DummyListener") + { + } + + public override bool IsEnabled(string name) => true; + + public override void Write(string name, object value) + { + } + } + + private class CallbackDiagnosticListener : IObserver<KeyValuePair<string, object>> + { + private readonly Action<KeyValuePair<string, object>> _callback; + + public CallbackDiagnosticListener(Action<KeyValuePair<string, object>> callback) + { + _callback = callback; + } + + public void OnNext(KeyValuePair<string, object> value) + { + _callback(value); + } + + public void OnError(Exception error) + { + } + + public void OnCompleted() + { + } + } + } +} diff --git a/src/Hosting/Hosting/test/HostingEnvironmentExtensionsTests.cs b/src/Hosting/Hosting/test/HostingEnvironmentExtensionsTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..3a646b005b73d13a338219433a282299f790c71a --- /dev/null +++ b/src/Hosting/Hosting/test/HostingEnvironmentExtensionsTests.cs @@ -0,0 +1,63 @@ +// 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.IO; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.Extensions.FileProviders; +using Xunit; + +namespace Microsoft.AspNetCore.Hosting.Tests +{ + public class HostingEnvironmentExtensionsTests + { + [Fact] + public void SetsFullPathToWwwroot() + { + var env = new HostingEnvironment(); + + env.Initialize(Path.GetFullPath("."), new WebHostOptions() { WebRoot = "testroot" }); + + Assert.Equal(Path.GetFullPath("."), env.ContentRootPath); + Assert.Equal(Path.GetFullPath("testroot"), env.WebRootPath); + Assert.IsAssignableFrom<PhysicalFileProvider>(env.ContentRootFileProvider); + Assert.IsAssignableFrom<PhysicalFileProvider>(env.WebRootFileProvider); + } + + [Fact] + public void DefaultsToWwwrootSubdir() + { + var env = new HostingEnvironment(); + + env.Initialize(Path.GetFullPath("testroot"), new WebHostOptions()); + + Assert.Equal(Path.GetFullPath("testroot"), env.ContentRootPath); + Assert.Equal(Path.GetFullPath(Path.Combine("testroot", "wwwroot")), env.WebRootPath); + Assert.IsAssignableFrom<PhysicalFileProvider>(env.ContentRootFileProvider); + Assert.IsAssignableFrom<PhysicalFileProvider>(env.WebRootFileProvider); + } + + [Fact] + public void DefaultsToNullFileProvider() + { + var env = new HostingEnvironment(); + + env.Initialize(Path.GetFullPath(Path.Combine("testroot", "wwwroot")), new WebHostOptions()); + + Assert.Equal(Path.GetFullPath(Path.Combine("testroot", "wwwroot")), env.ContentRootPath); + Assert.Null(env.WebRootPath); + Assert.IsAssignableFrom<PhysicalFileProvider>(env.ContentRootFileProvider); + Assert.IsAssignableFrom<NullFileProvider>(env.WebRootFileProvider); + } + + [Fact] + public void OverridesEnvironmentFromConfig() + { + var env = new HostingEnvironment(); + env.EnvironmentName = "SomeName"; + + env.Initialize(Path.GetFullPath("."), new WebHostOptions() { Environment = "NewName" }); + + Assert.Equal("NewName", env.EnvironmentName); + } + } +} diff --git a/src/Hosting/Hosting/test/Internal/HostingEventSourceTests.cs b/src/Hosting/Hosting/test/Internal/HostingEventSourceTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..32b51d8a9560f388fe4a1f64d18a894a985b92b9 --- /dev/null +++ b/src/Hosting/Hosting/test/Internal/HostingEventSourceTests.cs @@ -0,0 +1,217 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.Tracing; +using System.Reflection; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + public class HostingEventSourceTests + { + [Fact] + public void MatchesNameAndGuid() + { + // Arrange & Act + var eventSourceType = typeof(WebHost).GetTypeInfo().Assembly.GetType( + "Microsoft.AspNetCore.Hosting.Internal.HostingEventSource", + throwOnError: true, + ignoreCase: false); + + // Assert + Assert.NotNull(eventSourceType); + Assert.Equal("Microsoft-AspNetCore-Hosting", EventSource.GetName(eventSourceType)); + Assert.Equal(Guid.Parse("9e620d2a-55d4-5ade-deb7-c26046d245a8"), EventSource.GetGuid(eventSourceType)); + Assert.NotEmpty(EventSource.GenerateManifest(eventSourceType, "assemblyPathToIncludeInManifest")); + } + + [Fact] + public void HostStart() + { + // Arrange + var expectedEventId = 1; + var eventListener = new TestEventListener(expectedEventId); + var hostingEventSource = HostingEventSource.Log; + eventListener.EnableEvents(hostingEventSource, EventLevel.Informational); + + // Act + hostingEventSource.HostStart(); + + // Assert + var eventData = eventListener.EventData; + Assert.NotNull(eventData); + Assert.Equal(expectedEventId, eventData.EventId); + Assert.Equal("HostStart", eventData.EventName); + Assert.Equal(EventLevel.Informational, eventData.Level); + Assert.Same(hostingEventSource, eventData.EventSource); + Assert.Null(eventData.Message); + Assert.Empty(eventData.Payload); + } + + [Fact] + public void HostStop() + { + // Arrange + var expectedEventId = 2; + var eventListener = new TestEventListener(expectedEventId); + var hostingEventSource = HostingEventSource.Log; + eventListener.EnableEvents(hostingEventSource, EventLevel.Informational); + + // Act + hostingEventSource.HostStop(); + + // Assert + var eventData = eventListener.EventData; + Assert.NotNull(eventData); + Assert.Equal(expectedEventId, eventData.EventId); + Assert.Equal("HostStop", eventData.EventName); + Assert.Equal(EventLevel.Informational, eventData.Level); + Assert.Same(hostingEventSource, eventData.EventSource); + Assert.Null(eventData.Message); + Assert.Empty(eventData.Payload); + } + + public static TheoryData<DefaultHttpContext, string[]> RequestStartData + { + get + { + var variations = new TheoryData<DefaultHttpContext, string[]>(); + + var context = new DefaultHttpContext(); + context.Request.Method = "GET"; + context.Request.Path = "/Home/Index"; + variations.Add( + context, + new string[] + { + "GET", + "/Home/Index" + }); + + context = new DefaultHttpContext(); + context.Request.Method = "POST"; + context.Request.Path = "/"; + variations.Add( + context, + new string[] + { + "POST", + "/" + }); + + return variations; + } + } + + [Theory] + [MemberData(nameof(RequestStartData))] + public void RequestStart(DefaultHttpContext httpContext, string[] expected) + { + // Arrange + var expectedEventId = 3; + var eventListener = new TestEventListener(expectedEventId); + var hostingEventSource = HostingEventSource.Log; + eventListener.EnableEvents(hostingEventSource, EventLevel.Informational); + + // Act + hostingEventSource.RequestStart(httpContext.Request.Method, httpContext.Request.Path); + + // Assert + var eventData = eventListener.EventData; + Assert.NotNull(eventData); + Assert.Equal(expectedEventId, eventData.EventId); + Assert.Equal("RequestStart", eventData.EventName); + Assert.Equal(EventLevel.Informational, eventData.Level); + Assert.Same(hostingEventSource, eventData.EventSource); + Assert.Null(eventData.Message); + + var payloadList = eventData.Payload; + Assert.Equal(expected.Length, payloadList.Count); + for (var i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], payloadList[i]); + } + } + + [Fact] + public void RequestStop() + { + // Arrange + var expectedEventId = 4; + var eventListener = new TestEventListener(expectedEventId); + var hostingEventSource = HostingEventSource.Log; + eventListener.EnableEvents(hostingEventSource, EventLevel.Informational); + + // Act + hostingEventSource.RequestStop(); + + // Assert + var eventData = eventListener.EventData; + Assert.Equal(expectedEventId, eventData.EventId); + Assert.Equal("RequestStop", eventData.EventName); + Assert.Equal(EventLevel.Informational, eventData.Level); + Assert.Same(hostingEventSource, eventData.EventSource); + Assert.Null(eventData.Message); + Assert.Empty(eventData.Payload); + } + + [Fact] + public void UnhandledException() + { + // Arrange + var expectedEventId = 5; + var eventListener = new TestEventListener(expectedEventId); + var hostingEventSource = HostingEventSource.Log; + eventListener.EnableEvents(hostingEventSource, EventLevel.Informational); + + // Act + hostingEventSource.UnhandledException(); + + // Assert + var eventData = eventListener.EventData; + Assert.Equal(expectedEventId, eventData.EventId); + Assert.Equal("UnhandledException", eventData.EventName); + Assert.Equal(EventLevel.Error, eventData.Level); + Assert.Same(hostingEventSource, eventData.EventSource); + Assert.Null(eventData.Message); + Assert.Empty(eventData.Payload); + } + + private static Exception GetException() + { + try + { + throw new InvalidOperationException("An invalid operation has occurred"); + } + catch (Exception ex) + { + return ex; + } + } + + private class TestEventListener : EventListener + { + private readonly int _eventId; + + public TestEventListener(int eventId) + { + _eventId = eventId; + } + + public EventWrittenEventArgs EventData { get; private set; } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + // The tests here run in parallel and since the single publisher instance (HostingEventingSource) + // notifies all listener instances in these tests, capture the EventData that a test is explicitly + // looking for and not give back other tests' data. + if (eventData.EventId == _eventId) + { + EventData = eventData; + } + } + } + } +} diff --git a/src/Hosting/Hosting/test/Internal/HostingRequestStartLogTests.cs b/src/Hosting/Hosting/test/Internal/HostingRequestStartLogTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..ca8e25ed9e0630bb0d45f94d0a19460dcf4cb12a --- /dev/null +++ b/src/Hosting/Hosting/test/Internal/HostingRequestStartLogTests.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Hosting.Internal; +using Moq; +using Xunit; +namespace Microsoft.AspNetCore.Hosting.Tests.Internal +{ + public class HostingRequestStartLogTests + { + [Theory] + [InlineData(",XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "Request starting GET 1.1 http://,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX//?query test 0")] + [InlineData(" XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "Request starting GET 1.1 http:// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX//?query test 0")] + public void InvalidHttpContext_DoesNotThrowOnAccessingProperties(string input, string expected) + { + var mockRequest = new Mock<HttpRequest>(); + mockRequest.Setup(request => request.Protocol).Returns("GET"); + mockRequest.Setup(request => request.Method).Returns("1.1"); + mockRequest.Setup(request => request.Scheme).Returns("http"); + mockRequest.Setup(request => request.Host).Returns(new HostString(input)); + mockRequest.Setup(request => request.PathBase).Returns(new PathString("/")); + mockRequest.Setup(request => request.Path).Returns(new PathString("/")); + mockRequest.Setup(request => request.QueryString).Returns(new QueryString("?query")); + mockRequest.Setup(request => request.ContentType).Returns("test"); + mockRequest.Setup(request => request.ContentLength).Returns(0); + + var mockContext = new Mock<HttpContext>(); + mockContext.Setup(context => context.Request).Returns(mockRequest.Object); + + var logger = new HostingRequestStartingLog(mockContext.Object); + Assert.Equal(expected, logger.ToString()); + } + } +} diff --git a/src/Hosting/Hosting/test/Internal/MyBadContainerFactory.cs b/src/Hosting/Hosting/test/Internal/MyBadContainerFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..058abc894a8c399581b9b3fd5c2c5c9d33158af0 --- /dev/null +++ b/src/Hosting/Hosting/test/Internal/MyBadContainerFactory.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Tests.Internal +{ + public class MyBadContainerFactory : IServiceProviderFactory<MyContainer> + { + public MyContainer CreateBuilder(IServiceCollection services) + { + var container = new MyContainer(); + container.Populate(services); + return container; + } + + public IServiceProvider CreateServiceProvider(MyContainer containerBuilder) + { + containerBuilder.Build(); + return null; + } + } +} diff --git a/src/Hosting/Hosting/test/Internal/MyContainer.cs b/src/Hosting/Hosting/test/Internal/MyContainer.cs new file mode 100644 index 0000000000000000000000000000000000000000..5fbffaad1b8cb5353d7ec4f1483b04b494570dda --- /dev/null +++ b/src/Hosting/Hosting/test/Internal/MyContainer.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Tests.Internal +{ + public class MyContainer : IServiceProvider + { + private IServiceProvider _inner; + private IServiceCollection _services; + + public bool FancyMethodCalled { get; private set; } + + public IServiceCollection Services => _services; + + public string Environment { get; set; } + + public object GetService(Type serviceType) + { + return _inner.GetService(serviceType); + } + + public void Populate(IServiceCollection services) + { + _services = services; + } + + public void Build() + { + _inner = _services.BuildServiceProvider(); + } + + public void MyFancyContainerMethod() + { + FancyMethodCalled = true; + } + } +} diff --git a/src/Hosting/Hosting/test/Internal/MyContainerFactory.cs b/src/Hosting/Hosting/test/Internal/MyContainerFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..06a1ad614b312e71505005114324583b2d216786 --- /dev/null +++ b/src/Hosting/Hosting/test/Internal/MyContainerFactory.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Tests.Internal +{ + public class MyContainerFactory : IServiceProviderFactory<MyContainer> + { + public MyContainer CreateBuilder(IServiceCollection services) + { + var container = new MyContainer(); + container.Populate(services); + return container; + } + + public IServiceProvider CreateServiceProvider(MyContainer containerBuilder) + { + containerBuilder.Build(); + return containerBuilder; + } + } +} diff --git a/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj b/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..151a66777adbd1c7eaa6aa77b69ace28af152e3f --- /dev/null +++ b/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj @@ -0,0 +1,22 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks> + </PropertyGroup> + + <ItemGroup> + <Content Include="testroot\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\test\testassets\TestStartupAssembly1\TestStartupAssembly1.csproj" /> + </ItemGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Owin" /> + <Reference Include="Microsoft.AspNetCore.Hosting" /> + <Reference Include="Microsoft.Extensions.Logging.Testing" /> + <Reference Include="Microsoft.Extensions.Options" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/Hosting/test/RequestServicesContainerMiddlewareTests.cs b/src/Hosting/Hosting/test/RequestServicesContainerMiddlewareTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..c153af4dc1fec0da864cf1fda02841cd4c5b17b1 --- /dev/null +++ b/src/Hosting/Hosting/test/RequestServicesContainerMiddlewareTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Hosting.Tests +{ + public class RequestServicesContainerMiddlewareTests + { + [Fact] + public async Task RequestServicesAreSet() + { + var serviceProvider = new ServiceCollection() + .BuildServiceProvider(); + + var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>(); + + var middleware = new RequestServicesContainerMiddleware( + ctx => Task.CompletedTask, + scopeFactory); + + var context = new DefaultHttpContext(); + await middleware.Invoke(context); + + Assert.NotNull(context.RequestServices); + } + + [Fact] + public async Task RequestServicesAreNotOverwrittenIfAlreadySet() + { + var serviceProvider = new ServiceCollection() + .BuildServiceProvider(); + + var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>(); + + var middleware = new RequestServicesContainerMiddleware( + ctx => Task.CompletedTask, + scopeFactory); + + var context = new DefaultHttpContext(); + context.RequestServices = serviceProvider; + await middleware.Invoke(context); + + Assert.Same(serviceProvider, context.RequestServices); + } + + [Fact] + public async Task RequestServicesAreDisposedOnCompleted() + { + var serviceProvider = new ServiceCollection() + .AddTransient<DisposableThing>() + .BuildServiceProvider(); + + var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>(); + DisposableThing instance = null; + + var middleware = new RequestServicesContainerMiddleware( + ctx => + { + instance = ctx.RequestServices.GetRequiredService<DisposableThing>(); + return Task.CompletedTask; + }, + scopeFactory); + + var context = new DefaultHttpContext(); + var responseFeature = new TestHttpResponseFeature(); + context.Features.Set<IHttpResponseFeature>(responseFeature); + + await middleware.Invoke(context); + + Assert.NotNull(context.RequestServices); + Assert.Single(responseFeature.CompletedCallbacks); + + var callback = responseFeature.CompletedCallbacks[0]; + await callback.callback(callback.state); + + Assert.Null(context.RequestServices); + Assert.True(instance.Disposed); + } + + private class DisposableThing : IDisposable + { + public bool Disposed { get; set; } + public void Dispose() + { + Disposed = true; + } + } + + private class TestHttpResponseFeature : IHttpResponseFeature + { + public List<(Func<object, Task> callback, object state)> CompletedCallbacks = new List<(Func<object, Task> callback, object state)>(); + + public int StatusCode { get; set; } + public string ReasonPhrase { get; set; } + public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); + public Stream Body { get; set; } + + public bool HasStarted => false; + + public void OnCompleted(Func<object, Task> callback, object state) + { + CompletedCallbacks.Add((callback, state)); + } + + public void OnStarting(Func<object, Task> callback, object state) + { + } + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/StartupManagerTests.cs b/src/Hosting/Hosting/test/StartupManagerTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..75d5ccb98c1ad0b6474e8c41ce1e1c02110f3f5d --- /dev/null +++ b/src/Hosting/Hosting/test/StartupManagerTests.cs @@ -0,0 +1,735 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Hosting.Fakes; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Hosting.Tests.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Hosting.Tests +{ + public class StartupManagerTests + { + [Fact] + public void ConventionalStartupClass_StartupServiceFilters_WrapsConfigureServicesMethod() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>(); + serviceCollection.AddSingleton<IStartupConfigureServicesFilter>(new TestStartupServicesFilter(1, overrideAfterService: true)); + serviceCollection.AddSingleton<IStartupConfigureServicesFilter>(new TestStartupServicesFilter(2, overrideAfterService: true)); + var services = serviceCollection.BuildServiceProvider(); + + var type = typeof(VoidReturningStartupServicesFiltersStartup); + var startup = StartupLoader.LoadMethods(services, type, ""); + + var applicationServices = startup.ConfigureServicesDelegate(serviceCollection); + var before = applicationServices.GetRequiredService<ServiceBefore>(); + var after = applicationServices.GetRequiredService<ServiceAfter>(); + + Assert.Equal("StartupServicesFilter Before 1", before.Message); + Assert.Equal("StartupServicesFilter After 1", after.Message); + } + + [Fact] + public void ConventionalStartupClass_StartupServiceFilters_MultipleStartupServiceFiltersRun() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>(); + serviceCollection.AddSingleton<IStartupConfigureServicesFilter>(new TestStartupServicesFilter(1, overrideAfterService: false)); + serviceCollection.AddSingleton<IStartupConfigureServicesFilter>(new TestStartupServicesFilter(2, overrideAfterService: true)); + var services = serviceCollection.BuildServiceProvider(); + + var type = typeof(VoidReturningStartupServicesFiltersStartup); + var startup = StartupLoader.LoadMethods(services, type, ""); + + var applicationServices = startup.ConfigureServicesDelegate(serviceCollection); + var before = applicationServices.GetRequiredService<ServiceBefore>(); + var after = applicationServices.GetRequiredService<ServiceAfter>(); + + Assert.Equal("StartupServicesFilter Before 1", before.Message); + Assert.Equal("StartupServicesFilter After 2", after.Message); + } + + [Fact] + public void ConventionalStartupClass_StartupServicesFilters_ThrowsIfStartupBuildsTheContainerAsync() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>(); + serviceCollection.AddSingleton<IStartupConfigureServicesFilter>(new TestStartupServicesFilter(1, overrideAfterService: false)); + var services = serviceCollection.BuildServiceProvider(); + + var type = typeof(IServiceProviderReturningStartupServicesFiltersStartup); + var startup = StartupLoader.LoadMethods(services, type, ""); + + var expectedMessage = $"A ConfigureServices method that returns an {nameof(IServiceProvider)} is " + + $"not compatible with the use of one or more {nameof(IStartupConfigureServicesFilter)}. " + + $"Use a void returning ConfigureServices method instead or a ConfigureContainer method."; + + var exception = Assert.Throws<InvalidOperationException>(() => startup.ConfigureServicesDelegate(serviceCollection)); + + Assert.Equal(expectedMessage, exception.Message); + } + + [Fact] + public void ConventionalStartupClass_ConfigureContainerFilters_WrapInRegistrationOrder() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<MyContainer>, MyContainerFactory>(); + serviceCollection.AddSingleton<IStartupConfigureContainerFilter<MyContainer>>(new TestConfigureContainerFilter(1, overrideAfterService: true)); + serviceCollection.AddSingleton<IStartupConfigureContainerFilter<MyContainer>>(new TestConfigureContainerFilter(2, overrideAfterService: true)); + var services = serviceCollection.BuildServiceProvider(); + + var type = typeof(ConfigureContainerStartupServicesFiltersStartup); + var startup = StartupLoader.LoadMethods(services, type, ""); + + var applicationServices = startup.ConfigureServicesDelegate(serviceCollection); + var before = applicationServices.GetRequiredService<ServiceBefore>(); + var after = applicationServices.GetRequiredService<ServiceAfter>(); + + Assert.Equal("ConfigureContainerFilter Before 1", before.Message); + Assert.Equal("ConfigureContainerFilter After 1", after.Message); + } + + [Fact] + public void ConventionalStartupClass_ConfigureContainerFilters_RunsAllFilters() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<MyContainer>, MyContainerFactory>(); + serviceCollection.AddSingleton<IStartupConfigureContainerFilter<MyContainer>>(new TestConfigureContainerFilter(1, overrideAfterService: false)); + serviceCollection.AddSingleton<IStartupConfigureContainerFilter<MyContainer>>(new TestConfigureContainerFilter(2, overrideAfterService: true)); + var services = serviceCollection.BuildServiceProvider(); + + var type = typeof(ConfigureContainerStartupServicesFiltersStartup); + var startup = StartupLoader.LoadMethods(services, type, ""); + + var applicationServices = startup.ConfigureServicesDelegate(serviceCollection); + var before = applicationServices.GetRequiredService<ServiceBefore>(); + var after = applicationServices.GetRequiredService<ServiceAfter>(); + + Assert.Equal("ConfigureContainerFilter Before 1", before.Message); + Assert.Equal("ConfigureContainerFilter After 2", after.Message); + } + + [Fact] + public void ConventionalStartupClass_ConfigureContainerFilters_RunAfterConfigureServicesFilters() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<MyContainer>, MyContainerFactory>(); + serviceCollection.AddSingleton<IStartupConfigureServicesFilter>(new TestStartupServicesFilter(1, overrideAfterService: false)); + serviceCollection.AddSingleton<IStartupConfigureContainerFilter<MyContainer>>(new TestConfigureContainerFilter(2, overrideAfterService: true)); + var services = serviceCollection.BuildServiceProvider(); + + var type = typeof(ConfigureServicesAndConfigureContainerStartup); + var startup = StartupLoader.LoadMethods(services, type, ""); + + var applicationServices = startup.ConfigureServicesDelegate(serviceCollection); + var before = applicationServices.GetRequiredService<ServiceBefore>(); + var after = applicationServices.GetRequiredService<ServiceAfter>(); + + Assert.Equal("StartupServicesFilter Before 1", before.Message); + Assert.Equal("ConfigureContainerFilter After 2", after.Message); + } + + public class ConfigureContainerStartupServicesFiltersStartup + { + public void ConfigureContainer(MyContainer services) + { + services.Services.TryAddSingleton(new ServiceBefore { Message = "Configure container" }); + services.Services.TryAddSingleton(new ServiceAfter { Message = "Configure container" }); + } + + public void Configure(IApplicationBuilder builder) + { + } + } + + public class ConfigureServicesAndConfigureContainerStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.TryAddSingleton(new ServiceBefore { Message = "Configure services" }); + services.TryAddSingleton(new ServiceAfter { Message = "Configure services" }); + } + + public void ConfigureContainer(MyContainer services) + { + services.Services.TryAddSingleton(new ServiceBefore { Message = "Configure container" }); + services.Services.TryAddSingleton(new ServiceAfter { Message = "Configure container" }); + } + + public void Configure(IApplicationBuilder builder) + { + } + } + + public class TestConfigureContainerFilter : IStartupConfigureContainerFilter<MyContainer> + { + public TestConfigureContainerFilter(object additionalData, bool overrideAfterService) + { + AdditionalData = additionalData; + OverrideAfterService = overrideAfterService; + } + + public object AdditionalData { get; } + public bool OverrideAfterService { get; } + + public Action<MyContainer> ConfigureContainer(Action<MyContainer> next) + { + return services => + { + services.Services.TryAddSingleton(new ServiceBefore { Message = $"ConfigureContainerFilter Before {AdditionalData}" }); + + next(services); + + // Ensures we can always override. + if (OverrideAfterService) + { + services.Services.AddSingleton(new ServiceAfter { Message = $"ConfigureContainerFilter After {AdditionalData}" }); + } + else + { + services.Services.TryAddSingleton(new ServiceAfter { Message = $"ConfigureContainerFilter After {AdditionalData}" }); + } + }; + } + } + + public class IServiceProviderReturningStartupServicesFiltersStartup + { + public IServiceProvider ConfigureServices(IServiceCollection services) + { + services.TryAddSingleton(new ServiceBefore { Message = "Configure services" }); + services.TryAddSingleton(new ServiceAfter { Message = "Configure services" }); + + return services.BuildServiceProvider(); + } + + public void Configure(IApplicationBuilder builder) + { + } + } + + public class TestStartupServicesFilter : IStartupConfigureServicesFilter + { + public TestStartupServicesFilter(object additionalData, bool overrideAfterService) + { + AdditionalData = additionalData; + OverrideAfterService = overrideAfterService; + } + + public object AdditionalData { get; } + public bool OverrideAfterService { get; } + + public Action<IServiceCollection> ConfigureServices(Action<IServiceCollection> next) + { + return services => + { + services.TryAddSingleton(new ServiceBefore { Message = $"StartupServicesFilter Before {AdditionalData}" }); + + next(services); + + // Ensures we can always override. + if (OverrideAfterService) + { + services.AddSingleton(new ServiceAfter { Message = $"StartupServicesFilter After {AdditionalData}" }); + } + else + { + services.TryAddSingleton(new ServiceAfter { Message = $"StartupServicesFilter After {AdditionalData}" }); + } + }; + } + } + + public class VoidReturningStartupServicesFiltersStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.TryAddSingleton(new ServiceBefore { Message = "Configure services" }); + services.TryAddSingleton(new ServiceAfter { Message = "Configure services" }); + } + + public void Configure(IApplicationBuilder builder) + { + } + } + + + public class ServiceBefore + { + public string Message { get; set; } + } + + public class ServiceAfter + { + public string Message { get; set; } + } + + [Fact] + public void StartupClassMayHaveHostingServicesInjected() + { + var callbackStartup = new FakeStartupCallback(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>(); + serviceCollection.AddSingleton<IFakeStartupCallback>(callbackStartup); + var services = serviceCollection.BuildServiceProvider(); + + var type = StartupLoader.FindStartupType("Microsoft.AspNetCore.Hosting.Tests", "WithServices"); + var startup = StartupLoader.LoadMethods(services, type, "WithServices"); + + var app = new ApplicationBuilder(services); + app.ApplicationServices = startup.ConfigureServicesDelegate(serviceCollection); + startup.ConfigureDelegate(app); + + Assert.Equal(2, callbackStartup.MethodsCalled); + } + + [Fact] + public void StartupClassMayHaveScopedServicesInjected() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<IServiceCollection>>(new DefaultServiceProviderFactory(new ServiceProviderOptions + { + ValidateScopes = true + })); + + serviceCollection.AddScoped<DisposableService>(); + var services = serviceCollection.BuildServiceProvider(); + + var type = StartupLoader.FindStartupType("Microsoft.AspNetCore.Hosting.Tests", "WithScopedServices"); + var startup = StartupLoader.LoadMethods(services, type, "WithScopedServices"); + Assert.NotNull(startup.StartupInstance); + + var app = new ApplicationBuilder(services); + app.ApplicationServices = startup.ConfigureServicesDelegate(serviceCollection); + startup.ConfigureDelegate(app); + + var instance = (StartupWithScopedServices)startup.StartupInstance; + Assert.NotNull(instance.DisposableService); + Assert.True(instance.DisposableService.Disposed); + } + + [Theory] + [InlineData(null)] + [InlineData("Dev")] + [InlineData("Retail")] + [InlineData("Static")] + [InlineData("StaticProvider")] + [InlineData("Provider")] + [InlineData("ProviderArgs")] + [InlineData("BaseClass")] + public void StartupClassAddsConfigureServicesToApplicationServices(string environment) + { + var services = new ServiceCollection() + .AddSingleton<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>() + .BuildServiceProvider(); + var type = StartupLoader.FindStartupType("Microsoft.AspNetCore.Hosting.Tests", environment); + var startup = StartupLoader.LoadMethods(services, type, environment); + + var app = new ApplicationBuilder(services); + app.ApplicationServices = startup.ConfigureServicesDelegate(new ServiceCollection()); + startup.ConfigureDelegate(app); + + var options = app.ApplicationServices.GetRequiredService<IOptions<FakeOptions>>().Value; + Assert.NotNull(options); + Assert.True(options.Configured); + Assert.Equal(environment, options.Environment); + } + + [Fact] + public void StartupWithNoConfigureThrows() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>(); + serviceCollection.AddSingleton<IFakeStartupCallback>(new FakeStartupCallback()); + var services = serviceCollection.BuildServiceProvider(); + var type = StartupLoader.FindStartupType("Microsoft.AspNetCore.Hosting.Tests", "Boom"); + var ex = Assert.Throws<InvalidOperationException>(() => StartupLoader.LoadMethods(services, type, "Boom")); + Assert.Equal("A public method named 'ConfigureBoom' or 'Configure' could not be found in the 'Microsoft.AspNetCore.Hosting.Fakes.StartupBoom' type.", ex.Message); + } + + [Theory] + [InlineData("caseinsensitive")] + [InlineData("CaseInsensitive")] + [InlineData("CASEINSENSITIVE")] + [InlineData("CaSEiNSENsitiVE")] + public void FindsStartupClassCaseInsensitive(string environment) + { + var type = StartupLoader.FindStartupType("Microsoft.AspNetCore.Hosting.Tests", environment); + + Assert.Equal("StartupCaseInsensitive", type.Name); + } + + [Theory] + [InlineData("caseinsensitive")] + [InlineData("CaseInsensitive")] + [InlineData("CASEINSENSITIVE")] + [InlineData("CaSEiNSENsitiVE")] + public void StartupClassAddsConfigureServicesToApplicationServicesCaseInsensitive(string environment) + { + var services = new ServiceCollection() + .AddSingleton<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>() + .BuildServiceProvider(); + var type = StartupLoader.FindStartupType("Microsoft.AspNetCore.Hosting.Tests", environment); + var startup = StartupLoader.LoadMethods(services, type, environment); + + var app = new ApplicationBuilder(services); + app.ApplicationServices = startup.ConfigureServicesDelegate(new ServiceCollection()); + startup.ConfigureDelegate(app); // By this not throwing, it found "ConfigureCaseInsensitive" + + var options = app.ApplicationServices.GetRequiredService<IOptions<FakeOptions>>().Value; + Assert.NotNull(options); + Assert.True(options.Configured); + Assert.Equal("ConfigureCaseInsensitiveServices", options.Environment); + } + + [Fact] + public void StartupWithTwoConfiguresThrows() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>(); + serviceCollection.AddSingleton<IFakeStartupCallback>(new FakeStartupCallback()); + var services = serviceCollection.BuildServiceProvider(); + + var type = StartupLoader.FindStartupType("Microsoft.AspNetCore.Hosting.Tests", "TwoConfigures"); + + var ex = Assert.Throws<InvalidOperationException>(() => StartupLoader.LoadMethods(services, type, "TwoConfigures")); + Assert.Equal("Having multiple overloads of method 'Configure' is not supported.", ex.Message); + } + + [Fact] + public void StartupWithPrivateConfiguresThrows() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>(); + serviceCollection.AddSingleton<IFakeStartupCallback>(new FakeStartupCallback()); + var services = serviceCollection.BuildServiceProvider(); + + var diagnosticMessages = new List<string>(); + var type = StartupLoader.FindStartupType("Microsoft.AspNetCore.Hosting.Tests", "PrivateConfigure"); + + var ex = Assert.Throws<InvalidOperationException>(() => StartupLoader.LoadMethods(services, type, "PrivateConfigure")); + Assert.Equal("A public method named 'ConfigurePrivateConfigure' or 'Configure' could not be found in the 'Microsoft.AspNetCore.Hosting.Fakes.StartupPrivateConfigure' type.", ex.Message); + } + + [Fact] + public void StartupWithTwoConfigureServicesThrows() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>(); + serviceCollection.AddSingleton<IFakeStartupCallback>(new FakeStartupCallback()); + var services = serviceCollection.BuildServiceProvider(); + + var type = StartupLoader.FindStartupType("Microsoft.AspNetCore.Hosting.Tests", "TwoConfigureServices"); + + var ex = Assert.Throws<InvalidOperationException>(() => StartupLoader.LoadMethods(services, type, "TwoConfigureServices")); + Assert.Equal("Having multiple overloads of method 'ConfigureServices' is not supported.", ex.Message); + } + + [Fact] + public void StartupClassCanHandleConfigureServicesThatReturnsNull() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>(); + var services = serviceCollection.BuildServiceProvider(); + + var type = StartupLoader.FindStartupType("Microsoft.AspNetCore.Hosting.Tests", "WithNullConfigureServices"); + var startup = StartupLoader.LoadMethods(services, type, "WithNullConfigureServices"); + + var app = new ApplicationBuilder(services); + app.ApplicationServices = startup.ConfigureServicesDelegate(new ServiceCollection()); + Assert.NotNull(app.ApplicationServices); + startup.ConfigureDelegate(app); + Assert.NotNull(app.ApplicationServices); + } + + [Fact] + public void StartupClassWithConfigureServicesShouldMakeServiceAvailableInConfigure() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>(); + var services = serviceCollection.BuildServiceProvider(); + + var type = StartupLoader.FindStartupType("Microsoft.AspNetCore.Hosting.Tests", "WithConfigureServices"); + var startup = StartupLoader.LoadMethods(services, type, "WithConfigureServices"); + + var app = new ApplicationBuilder(services); + app.ApplicationServices = startup.ConfigureServicesDelegate(serviceCollection); + startup.ConfigureDelegate(app); + + var foo = app.ApplicationServices.GetRequiredService<StartupWithConfigureServices.IFoo>(); + Assert.True(foo.Invoked); + } + + [Fact] + public void StartupLoaderCanLoadByType() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>(); + var services = serviceCollection.BuildServiceProvider(); + + var hostingEnv = new HostingEnvironment(); + var startup = StartupLoader.LoadMethods(services, typeof(TestStartup), hostingEnv.EnvironmentName); + + var app = new ApplicationBuilder(services); + app.ApplicationServices = startup.ConfigureServicesDelegate(serviceCollection); + startup.ConfigureDelegate(app); + + var foo = app.ApplicationServices.GetRequiredService<SimpleService>(); + Assert.Equal("Configure", foo.Message); + } + + [Fact] + public void StartupLoaderCanLoadByTypeWithEnvironment() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>(); + var services = serviceCollection.BuildServiceProvider(); + + var startup = StartupLoader.LoadMethods(services, typeof(TestStartup), "No"); + + var app = new ApplicationBuilder(services); + app.ApplicationServices = startup.ConfigureServicesDelegate(serviceCollection); + + var ex = Assert.Throws<TargetInvocationException>(() => startup.ConfigureDelegate(app)); + Assert.IsAssignableFrom<InvalidOperationException>(ex.InnerException); + } + + [Fact] + public void CustomProviderFactoryCallsConfigureContainer() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<MyContainer>, MyContainerFactory>(); + var services = serviceCollection.BuildServiceProvider(); + + var startup = StartupLoader.LoadMethods(services, typeof(MyContainerStartup), EnvironmentName.Development); + + var app = new ApplicationBuilder(services); + app.ApplicationServices = startup.ConfigureServicesDelegate(serviceCollection); + + Assert.IsType<MyContainer>(app.ApplicationServices); + Assert.True(((MyContainer)app.ApplicationServices).FancyMethodCalled); + } + + [Fact] + public void CustomServiceProviderFactoryStartupBaseClassCallsConfigureContainer() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<MyContainer>, MyContainerFactory>(); + var services = serviceCollection.BuildServiceProvider(); + + var startup = StartupLoader.LoadMethods(services, typeof(MyContainerStartupBaseClass), EnvironmentName.Development); + + var app = new ApplicationBuilder(services); + app.ApplicationServices = startup.ConfigureServicesDelegate(serviceCollection); + + Assert.IsType<MyContainer>(app.ApplicationServices); + Assert.True(((MyContainer)app.ApplicationServices).FancyMethodCalled); + } + + [Fact] + public void CustomServiceProviderFactoryEnvironmentBasedConfigureContainer() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<MyContainer>, MyContainerFactory>(); + var services = serviceCollection.BuildServiceProvider(); + + var startup = StartupLoader.LoadMethods(services, typeof(MyContainerStartupEnvironmentBased), EnvironmentName.Production); + + var app = new ApplicationBuilder(services); + app.ApplicationServices = startup.ConfigureServicesDelegate(serviceCollection); + + Assert.IsType<MyContainer>(app.ApplicationServices); + Assert.Equal(((MyContainer)app.ApplicationServices).Environment, EnvironmentName.Production); + } + + [Fact] + public void CustomServiceProviderFactoryThrowsIfNotRegisteredWithConfigureContainerMethod() + { + var serviceCollection = new ServiceCollection(); + var services = serviceCollection.BuildServiceProvider(); + + var startup = StartupLoader.LoadMethods(services, typeof(MyContainerStartup), EnvironmentName.Development); + + Assert.Throws<InvalidOperationException>(() => startup.ConfigureServicesDelegate(serviceCollection)); + } + + [Fact] + public void CustomServiceProviderFactoryThrowsIfNotRegisteredWithConfigureContainerMethodStartupBase() + { + var serviceCollection = new ServiceCollection(); + var services = serviceCollection.BuildServiceProvider(); + + Assert.Throws<InvalidOperationException>(() => StartupLoader.LoadMethods(services, typeof(MyContainerStartupBaseClass), EnvironmentName.Development)); + } + + [Fact] + public void CustomServiceProviderFactoryFailsWithOverloadsInStartup() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<MyContainer>, MyContainerFactory>(); + var services = serviceCollection.BuildServiceProvider(); + + Assert.Throws<InvalidOperationException>(() => StartupLoader.LoadMethods(services, typeof(MyContainerStartupWithOverloads), EnvironmentName.Development)); + } + + [Fact] + public void BadServiceProviderFactoryFailsThatReturnsNullServiceProviderOverriddenByDefault() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton<IServiceProviderFactory<MyContainer>, MyBadContainerFactory>(); + var services = serviceCollection.BuildServiceProvider(); + + var startup = StartupLoader.LoadMethods(services, typeof(MyContainerStartup), EnvironmentName.Development); + + var app = new ApplicationBuilder(services); + app.ApplicationServices = startup.ConfigureServicesDelegate(serviceCollection); + + Assert.NotNull(app.ApplicationServices); + Assert.IsNotType<MyContainer>(app.ApplicationServices); + } + + public class MyContainerStartupWithOverloads + { + public void ConfigureServices(IServiceCollection services) + { + + } + + public void ConfigureContainer(MyContainer container) + { + container.MyFancyContainerMethod(); + } + + public void ConfigureContainer(IServiceCollection services) + { + + } + + public void Configure(IApplicationBuilder app) + { + + } + } + + public class MyContainerStartupEnvironmentBased + { + public void ConfigureServices(IServiceCollection services) + { + + } + + public void ConfigureDevelopmentContainer(MyContainer container) + { + container.Environment = EnvironmentName.Development; + } + + public void ConfigureProductionContainer(MyContainer container) + { + container.Environment = EnvironmentName.Production; + } + + public void Configure(IApplicationBuilder app) + { + + } + } + + public class MyContainerStartup + { + public void ConfigureServices(IServiceCollection services) + { + + } + + public void ConfigureContainer(MyContainer container) + { + container.MyFancyContainerMethod(); + } + + public void Configure(IApplicationBuilder app) + { + + } + } + + public class MyContainerStartupBaseClass : StartupBase<MyContainer> + { + public MyContainerStartupBaseClass(IServiceProviderFactory<MyContainer> factory) : base(factory) + { + } + + public override void Configure(IApplicationBuilder app) + { + + } + + public override void ConfigureContainer(MyContainer containerBuilder) + { + containerBuilder.MyFancyContainerMethod(); + } + } + + public class SimpleService + { + public SimpleService() + { + } + + public string Message { get; set; } + } + + public class TestStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton<SimpleService>(); + } + + public void ConfigureNoServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app) + { + var service = app.ApplicationServices.GetRequiredService<SimpleService>(); + service.Message = "Configure"; + } + + public void ConfigureNo(IApplicationBuilder app) + { + var service = app.ApplicationServices.GetRequiredService<SimpleService>(); + } + } + + public class FakeStartupCallback : IFakeStartupCallback + { + private readonly IList<object> _configurationMethodCalledList = new List<object>(); + + public int MethodsCalled => _configurationMethodCalledList.Count; + + public void ConfigurationMethodCalled(object instance) + { + _configurationMethodCalledList.Add(instance); + } + } + + public class DisposableService : IDisposable + { + public bool Disposed { get; set; } + + public void Dispose() + { + Disposed = true; + } + } + } +} diff --git a/src/Hosting/Hosting/test/WebHostBuilderTests.cs b/src/Hosting/Hosting/test/WebHostBuilderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..c1244e5c8f7088036bbc49a4334c2a9a7b84f787 --- /dev/null +++ b/src/Hosting/Hosting/test/WebHostBuilderTests.cs @@ -0,0 +1,1234 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Fakes; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.ObjectPool; +using Xunit; + +[assembly: HostingStartup(typeof(WebHostBuilderTests.TestHostingStartup))] + +namespace Microsoft.AspNetCore.Hosting +{ + public class WebHostBuilderTests + { + [Fact] + public void Build_honors_UseStartup_with_string() + { + var builder = CreateWebHostBuilder().UseServer(new TestServer()); + + using (var host = (WebHost)builder.UseStartup("MyStartupAssembly").Build()) + { + Assert.Equal("MyStartupAssembly", host.Options.ApplicationName); + Assert.Equal("MyStartupAssembly", host.Options.StartupAssembly); + } + } + + [Fact] + public async Task StartupMissing_Fallback() + { + var builder = CreateWebHostBuilder(); + var server = new TestServer(); + using (var host = builder.UseServer(server).UseStartup("MissingStartupAssembly").Build()) + { + await host.StartAsync(); + await AssertResponseContains(server.RequestDelegate, "MissingStartupAssembly"); + } + } + + [Fact] + public async Task StartupStaticCtorThrows_Fallback() + { + var builder = CreateWebHostBuilder(); + var server = new TestServer(); + var host = builder.UseServer(server).UseStartup<StartupStaticCtorThrows>().Build(); + using (host) + { + await host.StartAsync(); + await AssertResponseContains(server.RequestDelegate, "Exception from static constructor"); + } + } + + [Fact] + public async Task StartupCtorThrows_Fallback() + { + var builder = CreateWebHostBuilder(); + var server = new TestServer(); + var host = builder.UseServer(server).UseStartup<StartupCtorThrows>().Build(); + using (host) + { + await host.StartAsync(); + await AssertResponseContains(server.RequestDelegate, "Exception from constructor"); + } + } + + [Fact] + public async Task StartupCtorThrows_TypeLoadException() + { + var builder = CreateWebHostBuilder(); + var server = new TestServer(); + var host = builder.UseServer(server).UseStartup<StartupThrowTypeLoadException>().Build(); + using (host) + { + await host.StartAsync(); + await AssertResponseContains(server.RequestDelegate, "Message from the LoaderException</div>"); + } + } + + [Fact] + public async Task IApplicationLifetimeRegisteredEvenWhenStartupCtorThrows_Fallback() + { + var builder = CreateWebHostBuilder(); + var server = new TestServer(); + var host = builder.UseServer(server).UseStartup<StartupCtorThrows>().Build(); + using (host) + { + await host.StartAsync(); + var services = host.Services.GetServices<IApplicationLifetime>(); + Assert.NotNull(services); + Assert.NotEmpty(services); + + await AssertResponseContains(server.RequestDelegate, "Exception from constructor"); + } + } + + [Fact] + public async Task DefaultObjectPoolProvider_IsRegistered() + { + var server = new TestServer(); + var host = CreateWebHostBuilder() + .UseServer(server) + .Configure(app => { }) + .Build(); + using (host) + { + await host.StartAsync(); + Assert.IsType<DefaultObjectPoolProvider>(host.Services.GetService<ObjectPoolProvider>()); + } + } + + [Fact] + public async Task StartupConfigureServicesThrows_Fallback() + { + var builder = CreateWebHostBuilder(); + var server = new TestServer(); + var host = builder.UseServer(server).UseStartup<StartupConfigureServicesThrows>().Build(); + using (host) + { + await host.StartAsync(); + await AssertResponseContains(server.RequestDelegate, "Exception from ConfigureServices"); + } + } + + [Fact] + public async Task StartupConfigureThrows_Fallback() + { + var builder = CreateWebHostBuilder(); + var server = new TestServer(); + var host = builder.UseServer(server).UseStartup<StartupConfigureServicesThrows>().Build(); + using (host) + { + await host.StartAsync(); + await AssertResponseContains(server.RequestDelegate, "Exception from Configure"); + } + } + + [Fact] + public void DefaultCreatesLoggerFactory() + { + var hostBuilder = new WebHostBuilder() + .UseServer(new TestServer()) + .UseStartup<StartupNoServices>(); + + using (var host = (WebHost)hostBuilder.Build()) + { + Assert.NotNull(host.Services.GetService<ILoggerFactory>()); + } + } + + [Fact] + public void ConfigureDefaultServiceProvider() + { + var hostBuilder = new WebHostBuilder() + .UseServer(new TestServer()) + .ConfigureServices(s => + { + s.AddTransient<ServiceD>(); + s.AddScoped<ServiceC>(); + }) + .Configure(app => + { + app.ApplicationServices.GetRequiredService<ServiceC>(); + }) + .UseDefaultServiceProvider(options => + { + options.ValidateScopes = true; + }); + + Assert.Throws<InvalidOperationException>(() => hostBuilder.Build().Start()); + } + + [Fact] + public void ConfigureDefaultServiceProviderWithContext() + { + var configurationCallbackCalled = false; + var hostBuilder = new WebHostBuilder() + .UseServer(new TestServer()) + .ConfigureServices(s => + { + s.AddTransient<ServiceD>(); + s.AddScoped<ServiceC>(); + }) + .Configure(app => + { + app.ApplicationServices.GetRequiredService<ServiceC>(); + }) + .UseDefaultServiceProvider((context, options) => + { + Assert.NotNull(context.HostingEnvironment); + Assert.NotNull(context.Configuration); + configurationCallbackCalled = true; + options.ValidateScopes = true; + }); + + Assert.Throws<InvalidOperationException>(() => hostBuilder.Build().Start()); + Assert.True(configurationCallbackCalled); + } + + [Fact] + public void MultipleConfigureLoggingInvokedInOrder() + { + var callCount = 0; //Verify ordering + var hostBuilder = new WebHostBuilder() + .ConfigureLogging(loggerFactory => + { + Assert.Equal(0, callCount++); + }) + .ConfigureLogging(loggerFactory => + { + Assert.Equal(1, callCount++); + }) + .UseServer(new TestServer()) + .UseStartup<StartupNoServices>(); + + using (hostBuilder.Build()) + { + Assert.Equal(2, callCount); + } + } + + [Fact] + public async Task MultipleStartupAssembliesSpecifiedOnlyAddAssemblyOnce() + { + var provider = new TestLoggerProvider(); + var assemblyName = "RandomName"; + var data = new Dictionary<string, string> + { + { WebHostDefaults.ApplicationKey, assemblyName }, + { WebHostDefaults.HostingStartupAssembliesKey, assemblyName } + }; + var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); + + var builder = CreateWebHostBuilder() + .UseConfiguration(config) + .ConfigureLogging((_, factory) => + { + factory.AddProvider(provider); + }) + .UseServer(new TestServer()); + + // Verify that there was only one exception throw rather than two. + using (var host = (WebHost)builder.Build()) + { + await host.StartAsync(); + var context = provider.Sink.Writes.Where(s => s.EventId.Id == LoggerEventIds.HostingStartupAssemblyException); + Assert.NotNull(context); + Assert.Single(context); + } + } + + [Fact] + public void HostingContextContainsAppConfigurationDuringConfigureLogging() + { + var hostBuilder = new WebHostBuilder() + .ConfigureAppConfiguration((context, configBuilder) => + configBuilder.AddInMemoryCollection( + new KeyValuePair<string, string>[] + { + new KeyValuePair<string, string>("key1", "value1") + })) + .ConfigureLogging((context, factory) => + { + Assert.Equal("value1", context.Configuration["key1"]); + }) + .UseServer(new TestServer()) + .UseStartup<StartupNoServices>(); + + using (hostBuilder.Build()) { } + } + + [Fact] + public void HostingContextContainsAppConfigurationDuringConfigureServices() + { + var hostBuilder = new WebHostBuilder() + .ConfigureAppConfiguration((context, configBuilder) => + configBuilder.AddInMemoryCollection( + new KeyValuePair<string, string>[] + { + new KeyValuePair<string, string>("key1", "value1") + })) + .ConfigureServices((context, factory) => + { + Assert.Equal("value1", context.Configuration["key1"]); + }) + .UseServer(new TestServer()) + .UseStartup<StartupNoServices>(); + + using (hostBuilder.Build()) { } + } + + [Fact] + public void ThereIsAlwaysConfiguration() + { + var hostBuilder = new WebHostBuilder() + .UseServer(new TestServer()) + .UseStartup<StartupNoServices>(); + + using (var host = (WebHost)hostBuilder.Build()) + { + Assert.NotNull(host.Services.GetService<IConfiguration>()); + } + } + + [Fact] + public void ConfigureConfigurationSettingsPropagated() + { + var hostBuilder = new WebHostBuilder() + .UseSetting("key1", "value1") + .ConfigureAppConfiguration((context, configBuilder) => + { + var config = configBuilder.Build(); + Assert.Equal("value1", config["key1"]); + }) + .UseServer(new TestServer()) + .UseStartup<StartupNoServices>(); + + using (hostBuilder.Build()) { } + } + + [Fact] + public void CanConfigureConfigurationAndRetrieveFromDI() + { + var hostBuilder = new WebHostBuilder() + .ConfigureAppConfiguration((_, configBuilder) => + { + configBuilder + .AddInMemoryCollection( + new KeyValuePair<string, string>[] + { + new KeyValuePair<string, string>("key1", "value1") + }) + .AddEnvironmentVariables(); + }) + .UseServer(new TestServer()) + .UseStartup<StartupNoServices>(); + + using (var host = (WebHost)hostBuilder.Build()) + { + var config = host.Services.GetService<IConfiguration>(); + Assert.NotNull(config); + Assert.Equal("value1", config["key1"]); + } + } + + [Fact] + public void DoNotCaptureStartupErrorsByDefault() + { + var hostBuilder = new WebHostBuilder() + .UseServer(new TestServer()) + .UseStartup<StartupBoom>(); + + var exception = Assert.Throws<InvalidOperationException>(() => hostBuilder.Build()); + Assert.Equal("A public method named 'ConfigureProduction' or 'Configure' could not be found in the 'Microsoft.AspNetCore.Hosting.Fakes.StartupBoom' type.", exception.Message); + } + + [Fact] + public void ServiceProviderDisposedOnBuildException() + { + var service = new DisposableService(); + var hostBuilder = new WebHostBuilder() + .UseServer(new TestServer()) + .ConfigureServices(services => + { + // Added as a factory since instances are never disposed by the container + services.AddSingleton(sp => service); + }) + .UseStartup<StartupWithResolvedDisposableThatThrows>(); + + Assert.Throws<InvalidOperationException>(() => hostBuilder.Build()); + Assert.True(service.Disposed); + } + + [Fact] + public void CaptureStartupErrorsHonored() + { + var hostBuilder = new WebHostBuilder() + .CaptureStartupErrors(false) + .UseServer(new TestServer()) + .UseStartup<StartupBoom>(); + + var exception = Assert.Throws<InvalidOperationException>(() => hostBuilder.Build()); + Assert.Equal("A public method named 'ConfigureProduction' or 'Configure' could not be found in the 'Microsoft.AspNetCore.Hosting.Fakes.StartupBoom' type.", exception.Message); + } + + [Fact] + public void ConfigureServices_CanBeCalledMultipleTimes() + { + var callCount = 0; // Verify ordering + var hostBuilder = new WebHostBuilder() + .UseServer(new TestServer()) + .ConfigureServices(services => + { + Assert.Equal(0, callCount++); + services.AddTransient<ServiceA>(); + }) + .ConfigureServices(services => + { + Assert.Equal(1, callCount++); + services.AddTransient<ServiceB>(); + }) + .Configure(app => { }); + + using (var host = hostBuilder.Build()) + { + Assert.Equal(2, callCount); + + Assert.NotNull(host.Services.GetRequiredService<ServiceA>()); + Assert.NotNull(host.Services.GetRequiredService<ServiceB>()); + } + } + + [Fact] + public void CodeBasedSettingsCodeBasedOverride() + { + var hostBuilder = new WebHostBuilder() + .UseSetting(WebHostDefaults.EnvironmentKey, "EnvA") + .UseSetting(WebHostDefaults.EnvironmentKey, "EnvB") + .UseServer(new TestServer()) + .UseStartup<StartupNoServices>(); + + using (var host = (WebHost)hostBuilder.Build()) + { + Assert.Equal("EnvB", host.Options.Environment); + } + } + + [Fact] + public void CodeBasedSettingsConfigBasedOverride() + { + var settings = new Dictionary<string, string> + { + { WebHostDefaults.EnvironmentKey, "EnvB" } + }; + + var config = new ConfigurationBuilder() + .AddInMemoryCollection(settings) + .Build(); + + var hostBuilder = new WebHostBuilder() + .UseSetting(WebHostDefaults.EnvironmentKey, "EnvA") + .UseConfiguration(config) + .UseServer(new TestServer()) + .UseStartup<StartupNoServices>(); + + using (var host = (WebHost)hostBuilder.Build()) + { + Assert.Equal("EnvB", host.Options.Environment); + } + } + + [Fact] + public void ConfigBasedSettingsCodeBasedOverride() + { + var settings = new Dictionary<string, string> + { + { WebHostDefaults.EnvironmentKey, "EnvA" } + }; + + var config = new ConfigurationBuilder() + .AddInMemoryCollection(settings) + .Build(); + + var hostBuilder = new WebHostBuilder() + .UseConfiguration(config) + .UseSetting(WebHostDefaults.EnvironmentKey, "EnvB") + .UseServer(new TestServer()) + .UseStartup<StartupNoServices>(); + + using (var host = (WebHost)hostBuilder.Build()) + { + Assert.Equal("EnvB", host.Options.Environment); + } + } + + [Fact] + public void ConfigBasedSettingsConfigBasedOverride() + { + var settings = new Dictionary<string, string> + { + { WebHostDefaults.EnvironmentKey, "EnvA" } + }; + + var config = new ConfigurationBuilder() + .AddInMemoryCollection(settings) + .Build(); + + var overrideSettings = new Dictionary<string, string> + { + { WebHostDefaults.EnvironmentKey, "EnvB" } + }; + + var overrideConfig = new ConfigurationBuilder() + .AddInMemoryCollection(overrideSettings) + .Build(); + + var hostBuilder = new WebHostBuilder() + .UseConfiguration(config) + .UseConfiguration(overrideConfig) + .UseServer(new TestServer()) + .UseStartup<StartupNoServices>(); + + using (var host = (WebHost)hostBuilder.Build()) + { + Assert.Equal("EnvB", host.Options.Environment); + } + } + + [Fact] + public void UseEnvironmentIsNotOverriden() + { + var vals = new Dictionary<string, string> + { + { "ENV", "Dev" }, + }; + var builder = new ConfigurationBuilder() + .AddInMemoryCollection(vals); + var config = builder.Build(); + + var expected = "MY_TEST_ENVIRONMENT"; + + + using (var host = new WebHostBuilder() + .UseConfiguration(config) + .UseEnvironment(expected) + .UseServer(new TestServer()) + .UseStartup("Microsoft.AspNetCore.Hosting.Tests") + .Build()) + { + Assert.Equal(expected, host.Services.GetService<IHostingEnvironment>().EnvironmentName); + Assert.Equal(expected, host.Services.GetService<Extensions.Hosting.IHostingEnvironment>().EnvironmentName); + } + } + + [Fact] + public void BuildAndDispose() + { + var vals = new Dictionary<string, string> + { + { "ENV", "Dev" }, + }; + var builder = new ConfigurationBuilder() + .AddInMemoryCollection(vals); + var config = builder.Build(); + + var expected = "MY_TEST_ENVIRONMENT"; + using (var host = new WebHostBuilder() + .UseConfiguration(config) + .UseEnvironment(expected) + .UseServer(new TestServer()) + .UseStartup("Microsoft.AspNetCore.Hosting.Tests") + .Build()) { } + } + + [Fact] + public void UseBasePathConfiguresBasePath() + { + var vals = new Dictionary<string, string> + { + { "ENV", "Dev" }, + }; + var builder = new ConfigurationBuilder() + .AddInMemoryCollection(vals); + var config = builder.Build(); + + using (var host = new WebHostBuilder() + .UseConfiguration(config) + .UseContentRoot("/") + .UseServer(new TestServer()) + .UseStartup("Microsoft.AspNetCore.Hosting.Tests") + .Build()) + { + Assert.Equal("/", host.Services.GetService<IHostingEnvironment>().ContentRootPath); + Assert.Equal("/", host.Services.GetService<Extensions.Hosting.IHostingEnvironment>().ContentRootPath); + } + } + + [Fact] + public void RelativeContentRootIsResolved() + { + using (var host = new WebHostBuilder() + .UseContentRoot("testroot") + .UseServer(new TestServer()) + .UseStartup("Microsoft.AspNetCore.Hosting.Tests") + .Build()) + { + var basePath = host.Services.GetRequiredService<IHostingEnvironment>().ContentRootPath; + var basePath2 = host.Services.GetService<Extensions.Hosting.IHostingEnvironment>().ContentRootPath; + + Assert.True(Path.IsPathRooted(basePath)); + Assert.EndsWith(Path.DirectorySeparatorChar + "testroot", basePath); + + Assert.True(Path.IsPathRooted(basePath2)); + Assert.EndsWith(Path.DirectorySeparatorChar + "testroot", basePath2); + } + } + + [Fact] + public void DefaultContentRootIsApplicationBasePath() + { + using (var host = new WebHostBuilder() + .UseServer(new TestServer()) + .UseStartup("Microsoft.AspNetCore.Hosting.Tests") + .Build()) + { + var appBase = AppContext.BaseDirectory; + Assert.Equal(appBase, host.Services.GetService<IHostingEnvironment>().ContentRootPath); + Assert.Equal(appBase, host.Services.GetService<Extensions.Hosting.IHostingEnvironment>().ContentRootPath); + } + } + + [Fact] + public void DefaultWebHostBuilderWithNoStartupThrows() + { + var host = new WebHostBuilder() + .UseServer(new TestServer()); + + var ex = Assert.Throws<InvalidOperationException>(() => host.Build()); + + Assert.Contains("No startup configured.", ex.Message); + } + + [Fact] + public void DefaultApplicationNameWithUseStartupOfString() + { + var builder = new ConfigurationBuilder(); + using (var host = new WebHostBuilder() + .UseServer(new TestServer()) + .UseStartup(typeof(Startup).Assembly.GetName().Name) + .Build()) + { + var hostingEnv = host.Services.GetService<IHostingEnvironment>(); + var hostingEnv2 = host.Services.GetService<Extensions.Hosting.IHostingEnvironment>(); + Assert.Equal(typeof(Startup).Assembly.GetName().Name, hostingEnv.ApplicationName); + Assert.Equal(typeof(Startup).Assembly.GetName().Name, hostingEnv2.ApplicationName); + } + } + + [Fact] + public void DefaultApplicationNameWithUseStartupOfT() + { + var builder = new ConfigurationBuilder(); + using (var host = new WebHostBuilder() + .UseServer(new TestServer()) + .UseStartup<StartupNoServices>() + .Build()) + { + var hostingEnv = host.Services.GetService<IHostingEnvironment>(); + var hostingEnv2 = host.Services.GetService<Extensions.Hosting.IHostingEnvironment>(); + Assert.Equal(typeof(StartupNoServices).Assembly.GetName().Name, hostingEnv.ApplicationName); + Assert.Equal(typeof(StartupNoServices).Assembly.GetName().Name, hostingEnv2.ApplicationName); + } + } + + [Fact] + public void DefaultApplicationNameWithUseStartupOfType() + { + var builder = new ConfigurationBuilder(); + var host = new WebHostBuilder() + .UseServer(new TestServer()) + .UseStartup(typeof(StartupNoServices)) + .Build(); + + var hostingEnv = host.Services.GetService<IHostingEnvironment>(); + Assert.Equal(typeof(StartupNoServices).Assembly.GetName().Name, hostingEnv.ApplicationName); + } + + [Fact] + public void DefaultApplicationNameWithConfigure() + { + var builder = new ConfigurationBuilder(); + using (var host = new WebHostBuilder() + .UseServer(new TestServer()) + .Configure(app => { }) + .Build()) + { + var hostingEnv = host.Services.GetService<IHostingEnvironment>(); + + // Should be the assembly containing this test, because that's where the delegate comes from + Assert.Equal(typeof(WebHostBuilderTests).Assembly.GetName().Name, hostingEnv.ApplicationName); + } + } + + [Fact] + public void Configure_SupportsNonStaticMethodDelegate() + { + using (var host = new WebHostBuilder() + .UseServer(new TestServer()) + .Configure(app => { }) + .Build()) + { + var hostingEnv = host.Services.GetService<IHostingEnvironment>(); + Assert.Equal("Microsoft.AspNetCore.Hosting.Tests", hostingEnv.ApplicationName); + } + } + + [Fact] + public void Configure_SupportsStaticMethodDelegate() + { + using (var host = new WebHostBuilder() + .UseServer(new TestServer()) + .Configure(StaticConfigureMethod) + .Build()) + { + var hostingEnv = host.Services.GetService<IHostingEnvironment>(); + Assert.Equal("Microsoft.AspNetCore.Hosting.Tests", hostingEnv.ApplicationName); + } + } + + [Fact] + public void Build_DoesNotAllowBuildingMuiltipleTimes() + { + var builder = CreateWebHostBuilder(); + var server = new TestServer(); + using (builder.UseServer(server) + .UseStartup<StartupNoServices>() + .Build()) + { + var ex = Assert.Throws<InvalidOperationException>(() => builder.Build()); + Assert.Equal("WebHostBuilder allows creation only of a single instance of WebHost", ex.Message); + } + } + + [Fact] + public void Build_DoesNotOverrideILoggerFactorySetByConfigureServices() + { + var factory = new DisposableLoggerFactory(); + var builder = CreateWebHostBuilder(); + var server = new TestServer(); + + using (var host = builder.UseServer(server) + .ConfigureServices(collection => collection.AddSingleton<ILoggerFactory>(factory)) + .UseStartup<StartupWithILoggerFactory>() + .Build()) + { + var factoryFromHost = host.Services.GetService<ILoggerFactory>(); + Assert.Equal(factory, factoryFromHost); + } + } + + [Fact] + public void Build_RunsHostingStartupAssembliesIfSpecified() + { + var builder = CreateWebHostBuilder() + .CaptureStartupErrors(false) + .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(TestStartupAssembly1.TestHostingStartup1).GetTypeInfo().Assembly.FullName) + .Configure(app => { }) + .UseServer(new TestServer()); + + using (var host = builder.Build()) + { + Assert.Equal("1", builder.GetSetting("testhostingstartup1")); + } + } + + [Fact] + public void Build_RunsHostingStartupRunsPrimaryAssemblyFirst() + { + var builder = CreateWebHostBuilder() + .CaptureStartupErrors(false) + .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(TestStartupAssembly1.TestHostingStartup1).GetTypeInfo().Assembly.FullName) + .Configure(app => { }) + .UseServer(new TestServer()); + + using (var host = builder.Build()) + { + Assert.Equal("0", builder.GetSetting("testhostingstartup")); + Assert.Equal("1", builder.GetSetting("testhostingstartup1")); + Assert.Equal("01", builder.GetSetting("testhostingstartup_chain")); + } + } + + [Fact] + public void Build_RunsHostingStartupAssembliesBeforeApplication() + { + var startup = new StartupVerifyServiceA(); + var startupAssemblyName = typeof(WebHostBuilderTests).GetTypeInfo().Assembly.GetName().Name; + + var builder = CreateWebHostBuilder() + .CaptureStartupErrors(false) + .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(WebHostBuilderTests).GetTypeInfo().Assembly.FullName) + .UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName) + .ConfigureServices(services => + { + services.AddSingleton<IStartup>(startup); + }) + .UseServer(new TestServer()); + + using (var host = builder.Build()) + { + host.Start(); + Assert.NotNull(startup.ServiceADescriptor); + Assert.NotNull(startup.ServiceA); + } + } + + + [Fact] + public async Task ExternalContainerInstanceCanBeUsedForEverything() + { + var disposables = new List<DisposableService>(); + + var containerFactory = new ExternalContainerFactory(services => + { + services.AddSingleton(sp => + { + var service = new DisposableService(); + disposables.Add(service); + return service; + }); + }); + + var host = new WebHostBuilder() + .UseStartup<StartupWithExternalServices>() + .UseServer(new TestServer()) + .ConfigureServices(services => + { + services.AddSingleton<IServiceProviderFactory<IServiceCollection>>(containerFactory); + }) + .Build(); + + using (host) + { + await host.StartAsync(); + } + + // We should create the hosting service provider and the application service provider + Assert.Equal(2, containerFactory.ServiceProviders.Count); + Assert.Equal(2, disposables.Count); + + Assert.NotEqual(disposables[0], disposables[1]); + Assert.True(disposables[0].Disposed); + Assert.True(disposables[1].Disposed); + } + + [Fact] + public void Build_HostingStartupAssemblyCanBeExcluded() + { + var builder = CreateWebHostBuilder() + .CaptureStartupErrors(false) + .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(TestStartupAssembly1.TestHostingStartup1).GetTypeInfo().Assembly.FullName) + .UseSetting(WebHostDefaults.HostingStartupExcludeAssembliesKey, typeof(TestStartupAssembly1.TestHostingStartup1).GetTypeInfo().Assembly.FullName) + .Configure(app => { }) + .UseServer(new TestServer()); + + using (var host = builder.Build()) + { + Assert.Null(builder.GetSetting("testhostingstartup1")); + Assert.Equal("0", builder.GetSetting("testhostingstartup_chain")); + } + } + + [Fact] + public void Build_ConfigureLoggingInHostingStartupWorks() + { + var builder = CreateWebHostBuilder() + .CaptureStartupErrors(false) + .Configure(app => + { + var loggerFactory = app.ApplicationServices.GetService<ILoggerFactory>(); + var logger = loggerFactory.CreateLogger(nameof(WebHostBuilderTests)); + logger.LogInformation("From startup"); + }) + .UseServer(new TestServer()); + + using (var host = (WebHost)builder.Build()) + { + host.Start(); + var sink = host.Services.GetRequiredService<ITestSink>(); + Assert.Contains(sink.Writes, w => w.State.ToString() == "From startup"); + } + } + + [Fact] + public void Build_ConfigureAppConfigurationInHostingStartupWorks() + { + var builder = CreateWebHostBuilder() + .CaptureStartupErrors(false) + .Configure(app => { }) + .UseServer(new TestServer()); + + using (var host = (WebHost)builder.Build()) + { + var configuration = host.Services.GetRequiredService<IConfiguration>(); + Assert.Equal("value", configuration["testhostingstartup:config"]); + } + } + + [Fact] + public void Build_DoesRunHostingStartupFromPrimaryAssemblyEvenIfNotSpecified() + { + var builder = CreateWebHostBuilder() + .Configure(app => { }) + .UseServer(new TestServer()); + + using (builder.Build()) + { + Assert.Equal("0", builder.GetSetting("testhostingstartup")); + } + } + + [Fact] + public void Build_HostingStartupFromPrimaryAssemblyCanBeDisabled() + { + var builder = CreateWebHostBuilder() + .UseSetting(WebHostDefaults.PreventHostingStartupKey, "true") + .Configure(app => { }) + .UseServer(new TestServer()); + + using (builder.Build()) + { + Assert.Null(builder.GetSetting("testhostingstartup")); + } + } + + [Fact] + public void Build_DoesntThrowIfUnloadableAssemblyNameInHostingStartupAssemblies() + { + var builder = CreateWebHostBuilder() + .CaptureStartupErrors(false) + .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "SomeBogusName") + .Configure(app => { }) + .UseServer(new TestServer()); + + using (builder.Build()) + { + Assert.Equal("0", builder.GetSetting("testhostingstartup")); + } + } + + [Fact] + public async Task Build_DoesNotThrowIfUnloadableAssemblyNameInHostingStartupAssembliesAndCaptureStartupErrorsTrue() + { + var provider = new TestLoggerProvider(); + var builder = CreateWebHostBuilder() + .ConfigureLogging((_, factory) => + { + factory.AddProvider(provider); + }) + .CaptureStartupErrors(true) + .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "SomeBogusName") + .Configure(app => { }) + .UseServer(new TestServer()); + + using (var host = builder.Build()) + { + await host.StartAsync(); + var context = provider.Sink.Writes.FirstOrDefault(s => s.EventId.Id == LoggerEventIds.HostingStartupAssemblyException); + Assert.NotNull(context); + } + } + + [Fact] + public void StartupErrorsAreLoggedIfCaptureStartupErrorsIsTrue() + { + var builder = CreateWebHostBuilder() + .CaptureStartupErrors(true) + .Configure(app => + { + throw new InvalidOperationException("Startup exception"); + }) + .UseServer(new TestServer()); + + using (var host = (WebHost)builder.Build()) + { + host.Start(); + var sink = host.Services.GetRequiredService<ITestSink>(); + Assert.Contains(sink.Writes, w => w.Exception?.Message == "Startup exception"); + } + } + + [Fact] + public void StartupErrorsAreLoggedIfCaptureStartupErrorsIsFalse() + { + ITestSink testSink = null; + + var builder = CreateWebHostBuilder() + .CaptureStartupErrors(false) + .Configure(app => + { + testSink = app.ApplicationServices.GetRequiredService<ITestSink>(); + + throw new InvalidOperationException("Startup exception"); + }) + .UseServer(new TestServer()); + + Assert.Throws<InvalidOperationException>(() => builder.Build().Start()); + + Assert.NotNull(testSink); + Assert.Contains(testSink.Writes, w => w.Exception?.Message == "Startup exception"); + } + + [Fact] + public void HostingStartupTypeCtorThrowsIfNull() + { + Assert.Throws<ArgumentNullException>(() => new HostingStartupAttribute(null)); + } + + [Fact] + public void HostingStartupTypeCtorThrowsIfNotIHosting() + { + Assert.Throws<ArgumentException>(() => new HostingStartupAttribute(typeof(WebHostTests))); + } + + [Fact] + public void UseShutdownTimeoutConfiguresShutdownTimeout() + { + var builder = CreateWebHostBuilder() + .CaptureStartupErrors(false) + .UseShutdownTimeout(TimeSpan.FromSeconds(102)) + .Configure(app => { }) + .UseServer(new TestServer()); + + using (var host = (WebHost)builder.Build()) + { + Assert.Equal(TimeSpan.FromSeconds(102), host.Options.ShutdownTimeout); + } + } + + private static void StaticConfigureMethod(IApplicationBuilder app) { } + + private IWebHostBuilder CreateWebHostBuilder() + { + var vals = new Dictionary<string, string> + { + { "DetailedErrors", "true" }, + { "captureStartupErrors", "true" } + }; + var builder = new ConfigurationBuilder() + .AddInMemoryCollection(vals); + var config = builder.Build(); + return new WebHostBuilder().UseConfiguration(config); + } + + private async Task AssertResponseContains(RequestDelegate app, string expectedText) + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.Body = new MemoryStream(); + await app(httpContext); + httpContext.Response.Body.Seek(0, SeekOrigin.Begin); + var bodyText = new StreamReader(httpContext.Response.Body).ReadToEnd(); + Assert.Contains(expectedText, bodyText); + } + + private class TestServer : IServer + { + IFeatureCollection IServer.Features { get; } + public RequestDelegate RequestDelegate { get; private set; } + + public void Dispose() { } + + public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) + { + RequestDelegate = async ctx => + { + var httpContext = application.CreateContext(ctx.Features); + try + { + await application.ProcessRequestAsync(httpContext); + } + catch (Exception ex) + { + application.DisposeContext(httpContext, ex); + throw; + } + application.DisposeContext(httpContext, null); + }; + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + internal class ExternalContainerFactory : IServiceProviderFactory<IServiceCollection> + { + private readonly Action<IServiceCollection> _configureServices; + private readonly List<IServiceProvider> _serviceProviders = new List<IServiceProvider>(); + + public List<IServiceProvider> ServiceProviders => _serviceProviders; + + public ExternalContainerFactory(Action<IServiceCollection> configureServices) + { + _configureServices = configureServices; + } + + public IServiceCollection CreateBuilder(IServiceCollection services) + { + _configureServices(services); + return services; + } + + public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder) + { + var provider = containerBuilder.BuildServiceProvider(); + _serviceProviders.Add(provider); + return provider; + } + } + + internal class StartupWithExternalServices + { + public DisposableService DisposableServiceCtor { get; set; } + + public DisposableService DisposableServiceApp { get; set; } + + public StartupWithExternalServices(DisposableService disposable) + { + DisposableServiceCtor = disposable; + } + + public void ConfigureServices(IServiceCollection services) { } + + public void Configure(IApplicationBuilder app, DisposableService disposable) + { + DisposableServiceApp = disposable; + } + } + + internal class StartupVerifyServiceA : IStartup + { + internal ServiceA ServiceA { get; set; } + + internal ServiceDescriptor ServiceADescriptor { get; set; } + + public IServiceProvider ConfigureServices(IServiceCollection services) + { + ServiceADescriptor = services.FirstOrDefault(s => s.ServiceType == typeof(ServiceA)); + + return services.BuildServiceProvider(); + } + + public void Configure(IApplicationBuilder app) + { + ServiceA = app.ApplicationServices.GetService<ServiceA>(); + } + } + + public class DisposableService : IDisposable + { + public bool Disposed { get; private set; } + + public void Dispose() + { + Disposed = true; + } + } + + public class TestHostingStartup : IHostingStartup + { + public void Configure(IWebHostBuilder builder) + { + var loggerProvider = new TestLoggerProvider(); + builder.UseSetting("testhostingstartup", "0") + .UseSetting("testhostingstartup_chain", builder.GetSetting("testhostingstartup_chain") + "0") + .ConfigureServices(services => services.AddSingleton<ServiceA>()) + .ConfigureServices(services => services.AddSingleton<ITestSink>(loggerProvider.Sink)) + .ConfigureLogging((_, lf) => lf.AddProvider(loggerProvider)) + .ConfigureAppConfiguration((context, configurationBuilder) => configurationBuilder.AddInMemoryCollection( + new[] + { + new KeyValuePair<string,string>("testhostingstartup:config", "value") + })); + } + } + + public class StartupWithResolvedDisposableThatThrows + { + public StartupWithResolvedDisposableThatThrows(DisposableService service) + { + + } + + public void ConfigureServices(IServiceCollection services) + { + throw new InvalidOperationException(); + } + + public void Configure(IApplicationBuilder app) + { + + } + } + + public class TestLoggerProvider : ILoggerProvider + { + public TestSink Sink { get; set; } = new TestSink(); + + public ILogger CreateLogger(string categoryName) => new TestLogger(categoryName, Sink, enabled: true); + + public void Dispose() { } + } + + private class ServiceC + { + public ServiceC(ServiceD serviceD) { } + } + + internal class ServiceD { } + + internal class ServiceA { } + + internal class ServiceB { } + + private class DisposableLoggerFactory : ILoggerFactory + { + public void Dispose() + { + Disposed = true; + } + + public bool Disposed { get; set; } + + public ILogger CreateLogger(string categoryName) => NullLogger.Instance; + + public void AddProvider(ILoggerProvider provider) { } + } + } +} diff --git a/src/Hosting/Hosting/test/WebHostConfigurationsTests.cs b/src/Hosting/Hosting/test/WebHostConfigurationsTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..88a43a4319ac77103383bc3bcf2bcef63ce9ddc7 --- /dev/null +++ b/src/Hosting/Hosting/test/WebHostConfigurationsTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Microsoft.AspNetCore.Hosting.Tests +{ + public class WebHostConfigurationTests + { + [Fact] + public void ReadsParametersCorrectly() + { + var parameters = new Dictionary<string, string>() + { + { WebHostDefaults.WebRootKey, "wwwroot"}, + { WebHostDefaults.ApplicationKey, "MyProjectReference"}, + { WebHostDefaults.StartupAssemblyKey, "MyProjectReference" }, + { WebHostDefaults.EnvironmentKey, EnvironmentName.Development}, + { WebHostDefaults.DetailedErrorsKey, "true"}, + { WebHostDefaults.CaptureStartupErrorsKey, "true" }, + { WebHostDefaults.SuppressStatusMessagesKey, "true" } + }; + + var config = new WebHostOptions(new ConfigurationBuilder().AddInMemoryCollection(parameters).Build()); + + Assert.Equal("wwwroot", config.WebRoot); + Assert.Equal("MyProjectReference", config.ApplicationName); + Assert.Equal("MyProjectReference", config.StartupAssembly); + Assert.Equal(EnvironmentName.Development, config.Environment); + Assert.True(config.CaptureStartupErrors); + Assert.True(config.DetailedErrors); + Assert.True(config.SuppressStatusMessages); + } + + [Fact] + public void ReadsOldEnvKey() + { + var parameters = new Dictionary<string, string>() { { "ENVIRONMENT", EnvironmentName.Development } }; + var config = new WebHostOptions(new ConfigurationBuilder().AddInMemoryCollection(parameters).Build()); + + Assert.Equal(EnvironmentName.Development, config.Environment); + } + + [Theory] + [InlineData("1", true)] + [InlineData("0", false)] + public void AllowsNumberForDetailedErrors(string value, bool expected) + { + var parameters = new Dictionary<string, string>() { { "detailedErrors", value } }; + var config = new WebHostOptions(new ConfigurationBuilder().AddInMemoryCollection(parameters).Build()); + + Assert.Equal(expected, config.DetailedErrors); + } + } +} diff --git a/src/Hosting/Hosting/test/WebHostTests.cs b/src/Hosting/Hosting/test/WebHostTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..0bc568277c5aaa47a4cea954539a93d2f323510b --- /dev/null +++ b/src/Hosting/Hosting/test/WebHostTests.cs @@ -0,0 +1,1328 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Fakes; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Hosting +{ + public class WebHostTests + { + [Fact] + public async Task WebHostThrowsWithNoServer() + { + var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => CreateBuilder().Build().StartAsync()); + Assert.Equal("No service for type 'Microsoft.AspNetCore.Hosting.Server.IServer' has been registered.", ex.Message); + } + + [Fact] + public void UseStartupThrowsWithNull() + { + Assert.Throws<ArgumentNullException>(() => CreateBuilder().UseStartup((string)null)); + } + + [Fact] + public async Task NoDefaultAddressesAndDoNotPreferHostingUrlsIfNotConfigured() + { + using (var host = CreateBuilder().UseFakeServer().Build()) + { + await host.StartAsync(); + var serverAddressesFeature = host.ServerFeatures.Get<IServerAddressesFeature>(); + Assert.False(serverAddressesFeature.Addresses.Any()); + Assert.False(serverAddressesFeature.PreferHostingUrls); + } + } + + [Fact] + public async Task UsesLegacyConfigurationForAddressesAndDoNotPreferHostingUrlsIfNotConfigured() + { + var data = new Dictionary<string, string> + { + { "server.urls", "http://localhost:5002" } + }; + + var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); + + using (var host = CreateBuilder(config).UseFakeServer().Build()) + { + await host.StartAsync(); + var serverAddressFeature = host.ServerFeatures.Get<IServerAddressesFeature>(); + Assert.Equal("http://localhost:5002", serverAddressFeature.Addresses.First()); + Assert.False(serverAddressFeature.PreferHostingUrls); + } + } + + [Fact] + public void UsesConfigurationForAddressesAndDoNotPreferHostingUrlsIfNotConfigured() + { + var data = new Dictionary<string, string> + { + { "urls", "http://localhost:5003" } + }; + + var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); + + using (var host = CreateBuilder(config).UseFakeServer().Build()) + { + host.Start(); + var serverAddressFeature = host.ServerFeatures.Get<IServerAddressesFeature>(); + Assert.Equal("http://localhost:5003", serverAddressFeature.Addresses.First()); + Assert.False(serverAddressFeature.PreferHostingUrls); + } + } + + [Fact] + public async Task UsesNewConfigurationOverLegacyConfigForAddressesAndDoNotPreferHostingUrlsIfNotConfigured() + { + var data = new Dictionary<string, string> + { + { "server.urls", "http://localhost:5003" }, + { "urls", "http://localhost:5009" } + }; + + var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); + + using (var host = CreateBuilder(config).UseFakeServer().Build()) + { + await host.StartAsync(); + var serverAddressFeature = host.ServerFeatures.Get<IServerAddressesFeature>(); + Assert.Equal("http://localhost:5009", serverAddressFeature.Addresses.First()); + Assert.False(serverAddressFeature.PreferHostingUrls); + } + } + + [Fact] + public void DoNotPreferHostingUrlsWhenNoAddressConfigured() + { + using (var host = CreateBuilder().UseFakeServer().PreferHostingUrls(true).Build()) + { + host.Start(); + var serverAddressesFeature = host.ServerFeatures.Get<IServerAddressesFeature>(); + Assert.Empty(serverAddressesFeature.Addresses); + Assert.False(serverAddressesFeature.PreferHostingUrls); + } + } + + [Fact] + public async Task PreferHostingUrlsWhenAddressIsConfigured() + { + var data = new Dictionary<string, string> + { + { "urls", "http://localhost:5003" } + }; + + var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); + + using (var host = CreateBuilder(config).UseFakeServer().PreferHostingUrls(true).Build()) + { + await host.StartAsync(); + Assert.True(host.ServerFeatures.Get<IServerAddressesFeature>().PreferHostingUrls); + } + } + + [Fact] + public void WebHostCanBeStarted() + { + using (var host = CreateBuilder() + .UseFakeServer() + .UseStartup("Microsoft.AspNetCore.Hosting.Tests") + .Start()) + { + var server = (FakeServer)host.Services.GetRequiredService<IServer>(); + Assert.NotNull(host); + Assert.Equal(1, server.StartInstances.Count); + Assert.Equal(0, server.StartInstances[0].DisposeCalls); + + host.Dispose(); + + Assert.Equal(1, server.StartInstances[0].DisposeCalls); + } + } + + [Fact] + public async Task WebHostShutsDownWhenTokenTriggers() + { + using (var host = CreateBuilder() + .UseFakeServer() + .UseStartup("Microsoft.AspNetCore.Hosting.Tests") + .Build()) + { + var lifetime = host.Services.GetRequiredService<IApplicationLifetime>(); + var lifetime2 = host.Services.GetRequiredService<Extensions.Hosting.IApplicationLifetime>(); + var server = (FakeServer)host.Services.GetRequiredService<IServer>(); + + var cts = new CancellationTokenSource(); + + var runInBackground = host.RunAsync(cts.Token); + + // Wait on the host to be started + lifetime.ApplicationStarted.WaitHandle.WaitOne(); + Assert.True(lifetime2.ApplicationStarted.IsCancellationRequested); + + Assert.Equal(1, server.StartInstances.Count); + Assert.Equal(0, server.StartInstances[0].DisposeCalls); + + cts.Cancel(); + + // Wait on the host to shutdown + lifetime.ApplicationStopped.WaitHandle.WaitOne(); + Assert.True(lifetime2.ApplicationStopped.IsCancellationRequested); + + // Wait for RunAsync to finish to guarantee Disposal of WebHost + await runInBackground; + + Assert.Equal(1, server.StartInstances[0].DisposeCalls); + } + } + + [Fact] + public async Task WebHostStopAsyncUsesDefaultTimeoutIfGivenTokenDoesNotFire() + { + var data = new Dictionary<string, string> + { + { WebHostDefaults.ShutdownTimeoutKey, "1" } + }; + + var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); + + var server = new Mock<IServer>(); + server.Setup(s => s.StopAsync(It.IsAny<CancellationToken>())) + .Returns<CancellationToken>(token => + { + return Task.Run(() => + { + token.WaitHandle.WaitOne(); + }); + }); + + using (var host = CreateBuilder(config) + .ConfigureServices(services => + { + services.AddSingleton(server.Object); + }) + .UseStartup("Microsoft.AspNetCore.Hosting.Tests") + .Build()) + { + await host.StartAsync(); + + var cts = new CancellationTokenSource(); + + // Purposefully don't trigger cts + var task = host.StopAsync(cts.Token); + + Assert.Equal(task, await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(10)))); + } + } + + [Fact] + public async Task WebHostStopAsyncUsesDefaultTimeoutIfNoTokenProvided() + { + var data = new Dictionary<string, string> + { + { WebHostDefaults.ShutdownTimeoutKey, "1" } + }; + + var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); + + var server = new Mock<IServer>(); + server.Setup(s => s.StopAsync(It.IsAny<CancellationToken>())) + .Returns<CancellationToken>(token => + { + return Task.Run(() => + { + token.WaitHandle.WaitOne(); + }); + }); + + using (var host = CreateBuilder(config) + .ConfigureServices(services => + { + services.AddSingleton(server.Object); + }) + .UseStartup("Microsoft.AspNetCore.Hosting.Tests") + .Build()) + { + await host.StartAsync(); + + var task = host.StopAsync(); + + Assert.Equal(task, await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(10)))); + } + } + + [Fact] + public async Task WebHostStopAsyncCanBeCancelledEarly() + { + var data = new Dictionary<string, string> + { + { WebHostDefaults.ShutdownTimeoutKey, "10" } + }; + + var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); + + var server = new Mock<IServer>(); + server.Setup(s => s.StopAsync(It.IsAny<CancellationToken>())) + .Returns<CancellationToken>(token => + { + return Task.Run(() => + { + token.WaitHandle.WaitOne(); + }); + }); + + using (var host = CreateBuilder(config) + .ConfigureServices(services => + { + services.AddSingleton(server.Object); + }) + .UseStartup("Microsoft.AspNetCore.Hosting.Tests") + .Build()) + { + await host.StartAsync(); + + var cts = new CancellationTokenSource(); + + var task = host.StopAsync(cts.Token); + cts.Cancel(); + + Assert.Equal(task, await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(8)))); + } + } + + [Fact] + public void WebHostApplicationLifetimeEventsOrderedCorrectlyDuringShutdown() + { + using (var host = CreateBuilder() + .UseFakeServer() + .UseStartup("Microsoft.AspNetCore.Hosting.Tests") + .Build()) + { + var lifetime = host.Services.GetRequiredService<IApplicationLifetime>(); + var applicationStartedEvent = new ManualResetEventSlim(false); + var applicationStoppingEvent = new ManualResetEventSlim(false); + var applicationStoppedEvent = new ManualResetEventSlim(false); + var applicationStartedCompletedBeforeApplicationStopping = false; + var applicationStoppingCompletedBeforeApplicationStopped = false; + var applicationStoppedCompletedBeforeRunCompleted = false; + + lifetime.ApplicationStarted.Register(() => + { + applicationStartedEvent.Set(); + }); + + lifetime.ApplicationStopping.Register(() => + { + // Check whether the applicationStartedEvent has been set + applicationStartedCompletedBeforeApplicationStopping = applicationStartedEvent.IsSet; + + // Simulate work. + Thread.Sleep(1000); + + applicationStoppingEvent.Set(); + }); + + lifetime.ApplicationStopped.Register(() => + { + // Check whether the applicationStoppingEvent has been set + applicationStoppingCompletedBeforeApplicationStopped = applicationStoppingEvent.IsSet; + applicationStoppedEvent.Set(); + }); + + var runHostAndVerifyApplicationStopped = Task.Run(async () => + { + await host.RunAsync(); + // Check whether the applicationStoppingEvent has been set + applicationStoppedCompletedBeforeRunCompleted = applicationStoppedEvent.IsSet; + }); + + // Wait until application has started to shut down the host + Assert.True(applicationStartedEvent.Wait(5000)); + + // Trigger host shutdown on a separate thread + Task.Run(() => lifetime.StopApplication()); + + // Wait for all events and host.Run() to complete + Assert.True(runHostAndVerifyApplicationStopped.Wait(5000)); + + // Verify Ordering + Assert.True(applicationStartedCompletedBeforeApplicationStopping); + Assert.True(applicationStoppingCompletedBeforeApplicationStopped); + Assert.True(applicationStoppedCompletedBeforeRunCompleted); + } + } + + [Fact] + public async Task WebHostDisposesServiceProvider() + { + using (var host = CreateBuilder() + .UseFakeServer() + .ConfigureServices(s => + { + s.AddTransient<IFakeService, FakeService>(); + s.AddSingleton<IFakeSingletonService, FakeService>(); + }) + .UseStartup("Microsoft.AspNetCore.Hosting.Tests") + .Build()) + { + await host.StartAsync(); + + var singleton = (FakeService)host.Services.GetService<IFakeSingletonService>(); + var transient = (FakeService)host.Services.GetService<IFakeService>(); + + Assert.False(singleton.Disposed); + Assert.False(transient.Disposed); + + await host.StopAsync(); + + Assert.False(singleton.Disposed); + Assert.False(transient.Disposed); + + host.Dispose(); + + Assert.True(singleton.Disposed); + Assert.True(transient.Disposed); + } + } + + [Fact] + public async Task WebHostNotifiesApplicationStarted() + { + using (var host = CreateBuilder() + .UseFakeServer() + .Build()) + { + var applicationLifetime = host.Services.GetService<IApplicationLifetime>(); + var applicationLifetime2 = host.Services.GetService<Extensions.Hosting.IApplicationLifetime>(); + + Assert.False(applicationLifetime.ApplicationStarted.IsCancellationRequested); + Assert.False(applicationLifetime2.ApplicationStarted.IsCancellationRequested); + + await host.StartAsync(); + Assert.True(applicationLifetime.ApplicationStarted.IsCancellationRequested); + Assert.True(applicationLifetime2.ApplicationStarted.IsCancellationRequested); + } + } + + [Fact] + public async Task WebHostNotifiesAllIApplicationLifetimeCallbacksEvenIfTheyThrow() + { + using (var host = CreateBuilder() + .UseFakeServer() + .Build()) + { + var applicationLifetime = host.Services.GetService<IApplicationLifetime>(); + var applicationLifetime2 = host.Services.GetService<Extensions.Hosting.IApplicationLifetime>(); + + var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted); + var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping); + var stopped = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopped); + + var started2 = RegisterCallbacksThatThrow(applicationLifetime2.ApplicationStarted); + var stopping2 = RegisterCallbacksThatThrow(applicationLifetime2.ApplicationStopping); + var stopped2 = RegisterCallbacksThatThrow(applicationLifetime2.ApplicationStopped); + + await host.StartAsync(); + Assert.True(applicationLifetime.ApplicationStarted.IsCancellationRequested); + Assert.True(applicationLifetime2.ApplicationStarted.IsCancellationRequested); + Assert.True(started.All(s => s)); + Assert.True(started2.All(s => s)); + host.Dispose(); + Assert.True(stopping.All(s => s)); + Assert.True(stopping2.All(s => s)); + Assert.True(stopped.All(s => s)); + Assert.True(stopped2.All(s => s)); + } + } + + [Fact] + public async Task WebHostNotifiesAllIApplicationLifetimeEventsCallbacksEvenIfTheyThrow() + { + bool[] events1 = null; + bool[] events2 = null; + + using (var host = CreateBuilder() + .UseFakeServer() + .ConfigureServices(services => + { + events1 = RegisterCallbacksThatThrow(services); + events2 = RegisterCallbacksThatThrow(services); + }) + .Build()) + { + await host.StartAsync(); + Assert.True(events1[0]); + Assert.True(events2[0]); + host.Dispose(); + Assert.True(events1[1]); + Assert.True(events2[1]); + } + } + + [Fact] + public async Task WebHostStopApplicationDoesNotFireStopOnHostedService() + { + var stoppingCalls = 0; + var disposingCalls = 0; + + using (var host = CreateBuilder() + .UseFakeServer() + .ConfigureServices(services => + { + Action started = () => + { + }; + + Action stopping = () => + { + stoppingCalls++; + }; + + Action disposing = () => + { + disposingCalls++; + }; + + services.AddSingleton<IHostedService>(_ => new DelegateHostedService(started, stopping, disposing)); + }) + .Build()) + { + var lifetime = host.Services.GetRequiredService<IApplicationLifetime>(); + lifetime.StopApplication(); + + await host.StartAsync(); + + Assert.Equal(0, stoppingCalls); + Assert.Equal(0, disposingCalls); + } + Assert.Equal(1, stoppingCalls); + Assert.Equal(1, disposingCalls); + } + + [Fact] + public async Task HostedServiceCanInjectApplicationLifetime() + { + using (var host = CreateBuilder() + .UseFakeServer() + .ConfigureServices(services => + { + services.AddSingleton<IHostedService, TestHostedService>(); + }) + .Build()) + { + var lifetime = host.Services.GetRequiredService<IApplicationLifetime>(); + lifetime.StopApplication(); + + await host.StartAsync(); + var svc = (TestHostedService)host.Services.GetRequiredService<IHostedService>(); + Assert.True(svc.StartCalled); + + await host.StopAsync(); + Assert.True(svc.StopCalled); + host.Dispose(); + } + } + + [Fact] + public async Task HostedServiceStartNotCalledIfWebHostNotStarted() + { + using (var host = CreateBuilder() + .UseFakeServer() + .ConfigureServices(services => + { + services.AddHostedService<TestHostedService>(); + }) + .Build()) + { + var lifetime = host.Services.GetRequiredService<IApplicationLifetime>(); + lifetime.StopApplication(); + + var svc = (TestHostedService)host.Services.GetRequiredService<IHostedService>(); + Assert.False(svc.StartCalled); + await host.StopAsync(); + Assert.False(svc.StopCalled); + host.Dispose(); + Assert.False(svc.StopCalled); + Assert.True(svc.DisposeCalled); + } + } + + [Fact] + public async Task WebHostStopApplicationFiresStopOnHostedService() + { + var stoppingCalls = 0; + var startedCalls = 0; + var disposingCalls = 0; + + using (var host = CreateBuilder() + .UseFakeServer() + .ConfigureServices(services => + { + Action started = () => + { + startedCalls++; + }; + + Action stopping = () => + { + stoppingCalls++; + }; + + Action disposing = () => + { + disposingCalls++; + }; + + services.AddSingleton<IHostedService>(_ => new DelegateHostedService(started, stopping, disposing)); + }) + .Build()) + { + var lifetime = host.Services.GetRequiredService<IApplicationLifetime>(); + + Assert.Equal(0, startedCalls); + + await host.StartAsync(); + Assert.Equal(1, startedCalls); + Assert.Equal(0, stoppingCalls); + Assert.Equal(0, disposingCalls); + + await host.StopAsync(); + + Assert.Equal(1, startedCalls); + Assert.Equal(1, stoppingCalls); + Assert.Equal(0, disposingCalls); + + host.Dispose(); + + Assert.Equal(1, startedCalls); + Assert.Equal(1, stoppingCalls); + Assert.Equal(1, disposingCalls); + } + } + + [Fact] + public async Task WebHostDisposeApplicationFiresStopOnHostedService() + { + var stoppingCalls = 0; + var startedCalls = 0; + var disposingCalls = 0; + + using (var host = CreateBuilder() + .UseFakeServer() + .ConfigureServices(services => + { + Action started = () => + { + startedCalls++; + }; + + Action stopping = () => + { + stoppingCalls++; + }; + + Action disposing = () => + { + disposingCalls++; + }; + + services.AddSingleton<IHostedService>(_ => new DelegateHostedService(started, stopping, disposing)); + }) + .Build()) + { + var lifetime = host.Services.GetRequiredService<IApplicationLifetime>(); + + Assert.Equal(0, startedCalls); + await host.StartAsync(); + Assert.Equal(1, startedCalls); + Assert.Equal(0, stoppingCalls); + Assert.Equal(0, disposingCalls); + host.Dispose(); + + Assert.Equal(1, stoppingCalls); + Assert.Equal(1, disposingCalls); + } + } + + [Fact] + public async Task WebHostNotifiesAllIHostedServicesAndIApplicationLifetimeCallbacksEvenIfTheyThrow() + { + bool[] events1 = null; + bool[] events2 = null; + + using (var host = CreateBuilder() + .UseFakeServer() + .ConfigureServices(services => + { + events1 = RegisterCallbacksThatThrow(services); + events2 = RegisterCallbacksThatThrow(services); + }) + .Build()) + { + var applicationLifetime = host.Services.GetService<IApplicationLifetime>(); + var applicationLifetime2 = host.Services.GetService<Extensions.Hosting.IApplicationLifetime>(); + + var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted); + var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping); + + var started2 = RegisterCallbacksThatThrow(applicationLifetime2.ApplicationStarted); + var stopping2 = RegisterCallbacksThatThrow(applicationLifetime2.ApplicationStopping); + + await host.StartAsync(); + Assert.True(events1[0]); + Assert.True(events2[0]); + Assert.True(started.All(s => s)); + Assert.True(started2.All(s => s)); + host.Dispose(); + Assert.True(events1[1]); + Assert.True(events2[1]); + Assert.True(stopping.All(s => s)); + Assert.True(stopping2.All(s => s)); + } + } + + [Fact] + public async Task WebHostInjectsHostingEnvironment() + { + using (var host = CreateBuilder() + .UseFakeServer() + .UseStartup("Microsoft.AspNetCore.Hosting.Tests") + .UseEnvironment("WithHostingEnvironment") + .Build()) + { + await host.StartAsync(); + var env = host.Services.GetService<IHostingEnvironment>(); + var env2 = host.Services.GetService<Extensions.Hosting.IHostingEnvironment>(); + Assert.Equal("Changed", env.EnvironmentName); + Assert.Equal("Changed", env2.EnvironmentName); + } + } + + [Fact] + public void CanReplaceStartupLoader() + { + var builder = CreateBuilder() + .ConfigureServices(services => + { + services.AddTransient<IStartup, TestStartup>(); + }) + .UseFakeServer() + .UseStartup("Microsoft.AspNetCore.Hosting.Tests"); + + Assert.Throws<NotImplementedException>(() => builder.Build()); + } + + [Fact] + public void CanCreateApplicationServicesWithAddedServices() + { + using (var host = CreateBuilder().UseFakeServer().ConfigureServices(services => services.AddOptions()).Build()) + { + Assert.NotNull(host.Services.GetRequiredService<IOptions<object>>()); + } + } + + [Fact] + public void ConfiguresStartupFiltersInCorrectOrder() + { + // Verify ordering + var configureOrder = 0; + using (var host = CreateBuilder() + .UseFakeServer() + .ConfigureServices(services => + { + services.AddTransient<IStartupFilter>(serviceProvider => new TestFilter( + () => Assert.Equal(1, configureOrder++), + () => Assert.Equal(2, configureOrder++), + () => Assert.Equal(5, configureOrder++))); + services.AddTransient<IStartupFilter>(serviceProvider => new TestFilter( + () => Assert.Equal(0, configureOrder++), + () => Assert.Equal(3, configureOrder++), + () => Assert.Equal(4, configureOrder++))); + }) + .Build()) + { + host.Start(); + Assert.Equal(6, configureOrder); + } + } + + private class TestFilter : IStartupFilter + { + private readonly Action _verifyConfigureOrder; + private readonly Action _verifyBuildBeforeOrder; + private readonly Action _verifyBuildAfterOrder; + + public TestFilter(Action verifyConfigureOrder, Action verifyBuildBeforeOrder, Action verifyBuildAfterOrder) + { + _verifyConfigureOrder = verifyConfigureOrder; + _verifyBuildBeforeOrder = verifyBuildBeforeOrder; + _verifyBuildAfterOrder = verifyBuildAfterOrder; + } + + public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) + { + _verifyConfigureOrder(); + return builder => + { + _verifyBuildBeforeOrder(); + next(builder); + _verifyBuildAfterOrder(); + }; + } + } + + [Fact] + public void EnvDefaultsToProductionIfNoConfig() + { + using (var host = CreateBuilder().UseFakeServer().Build()) + { + var env = host.Services.GetService<IHostingEnvironment>(); + var env2 = host.Services.GetService<Extensions.Hosting.IHostingEnvironment>(); + Assert.Equal(EnvironmentName.Production, env.EnvironmentName); + Assert.Equal(EnvironmentName.Production, env2.EnvironmentName); + } + } + + [Fact] + public void EnvDefaultsToConfigValueIfSpecified() + { + var vals = new Dictionary<string, string> + { + { "Environment", EnvironmentName.Staging } + }; + + var builder = new ConfigurationBuilder() + .AddInMemoryCollection(vals); + var config = builder.Build(); + + using (var host = CreateBuilder(config).UseFakeServer().Build()) + { + var env = host.Services.GetService<IHostingEnvironment>(); + var env2 = host.Services.GetService<Extensions.Hosting.IHostingEnvironment>(); + Assert.Equal(EnvironmentName.Staging, env.EnvironmentName); + Assert.Equal(EnvironmentName.Staging, env.EnvironmentName); + } + } + + [Fact] + public void WebRootCanBeResolvedFromTheConfig() + { + var vals = new Dictionary<string, string> + { + { "webroot", "testroot" } + }; + + var builder = new ConfigurationBuilder() + .AddInMemoryCollection(vals); + var config = builder.Build(); + + using (var host = CreateBuilder(config).UseFakeServer().Build()) + { + var env = host.Services.GetService<IHostingEnvironment>(); + Assert.Equal(Path.GetFullPath("testroot"), env.WebRootPath); + Assert.True(env.WebRootFileProvider.GetFileInfo("TextFile.txt").Exists); + } + } + + [Fact] + public async Task IsEnvironment_Extension_Is_Case_Insensitive() + { + using (var host = CreateBuilder().UseFakeServer().Build()) + { + await host.StartAsync(); + var env = host.Services.GetRequiredService<IHostingEnvironment>(); + Assert.True(env.IsEnvironment(EnvironmentName.Production)); + Assert.True(env.IsEnvironment("producTion")); + } + } + + [Fact] + public async Task WebHost_CreatesDefaultRequestIdentifierFeature_IfNotPresent() + { + // Arrange + HttpContext httpContext = null; + var requestDelegate = new RequestDelegate(innerHttpContext => + { + httpContext = innerHttpContext; + return Task.FromResult(0); + }); + + using (var host = CreateHost(requestDelegate)) + { + // Act + await host.StartAsync(); + + // Assert + Assert.NotNull(httpContext); + var featuresTraceIdentifier = httpContext.Features.Get<IHttpRequestIdentifierFeature>().TraceIdentifier; + Assert.False(string.IsNullOrWhiteSpace(httpContext.TraceIdentifier)); + Assert.Same(httpContext.TraceIdentifier, featuresTraceIdentifier); + } + } + + [Fact] + public async Task WebHost_DoesNot_CreateDefaultRequestIdentifierFeature_IfPresent() + { + // Arrange + HttpContext httpContext = null; + var requestDelegate = new RequestDelegate(innerHttpContext => + { + httpContext = innerHttpContext; + return Task.FromResult(0); + }); + var requestIdentifierFeature = new StubHttpRequestIdentifierFeature(); + + using (var host = CreateHost(requestDelegate)) + { + var server = (FakeServer)host.Services.GetRequiredService<IServer>(); + server.CreateRequestFeatures = () => + { + var features = FakeServer.NewFeatureCollection(); + features.Set<IHttpRequestIdentifierFeature>(requestIdentifierFeature); + return features; + }; + // Act + await host.StartAsync(); + + // Assert + Assert.NotNull(httpContext); + Assert.Same(requestIdentifierFeature, httpContext.Features.Get<IHttpRequestIdentifierFeature>()); + } + } + + [Fact] + public async Task WebHost_InvokesConfigureMethodsOnlyOnce() + { + using (var host = CreateBuilder() + .UseFakeServer() + .UseStartup<CountStartup>() + .Build()) + { + await host.StartAsync(); + var services = host.Services; + var services2 = host.Services; + Assert.Equal(1, CountStartup.ConfigureCount); + Assert.Equal(1, CountStartup.ConfigureServicesCount); + } + } + + public class CountStartup + { + public static int ConfigureServicesCount; + public static int ConfigureCount; + + public void ConfigureServices(IServiceCollection services) + { + ConfigureServicesCount++; + } + + public void Configure(IApplicationBuilder app) + { + ConfigureCount++; + } + } + + [Fact] + public void WebHost_ThrowsForBadConfigureServiceSignature() + { + var builder = CreateBuilder() + .UseFakeServer() + .UseStartup<BadConfigureServicesStartup>(); + + var ex = Assert.Throws<InvalidOperationException>(() => builder.Build()); + Assert.Contains("ConfigureServices", ex.Message); + } + + public class BadConfigureServicesStartup + { + public void ConfigureServices(IServiceCollection services, int gunk) { } + public void Configure(IApplicationBuilder app) { } + } + + private IWebHost CreateHost(RequestDelegate requestDelegate) + { + var builder = CreateBuilder() + .UseFakeServer() + .ConfigureLogging((_, factory) => + { + factory.AddProvider(new AllMessagesAreNeeded()); + }) + .Configure( + appBuilder => + { + appBuilder.Run(requestDelegate); + }); + return builder.Build(); + } + + private IWebHostBuilder CreateBuilder(IConfiguration config = null) + { + return new WebHostBuilder().UseConfiguration(config ?? new ConfigurationBuilder().Build()).UseStartup("Microsoft.AspNetCore.Hosting.Tests"); + } + + private static bool[] RegisterCallbacksThatThrow(IServiceCollection services) + { + bool[] events = new bool[2]; + + Action started = () => + { + events[0] = true; + throw new InvalidOperationException(); + }; + + Action stopping = () => + { + events[1] = true; + throw new InvalidOperationException(); + }; + + services.AddSingleton<IHostedService>(new DelegateHostedService(started, stopping, () => { })); + + return events; + } + + private static bool[] RegisterCallbacksThatThrow(CancellationToken token) + { + var signals = new bool[3]; + for (int i = 0; i < signals.Length; i++) + { + token.Register(state => + { + signals[(int)state] = true; + throw new InvalidOperationException(); + }, i); + } + + return signals; + } + + private class TestHostedService : IHostedService, IDisposable + { + private readonly IApplicationLifetime _lifetime; + + public TestHostedService(IApplicationLifetime lifetime, Extensions.Hosting.IApplicationLifetime lifetime2) + { + _lifetime = lifetime; + } + + public bool StartCalled { get; set; } + public bool StopCalled { get; set; } + public bool DisposeCalled { get; set; } + + public Task StartAsync(CancellationToken token) + { + StartCalled = true; + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken token) + { + StopCalled = true; + return Task.CompletedTask; + } + + public void Dispose() + { + DisposeCalled = true; + } + } + + private class DelegateHostedService : IHostedService, IDisposable + { + private readonly Action _started; + private readonly Action _stopping; + private readonly Action _disposing; + + public DelegateHostedService(Action started, Action stopping, Action disposing) + { + _started = started; + _stopping = stopping; + _disposing = disposing; + } + + public Task StartAsync(CancellationToken token) + { + _started(); + return Task.CompletedTask; + } + public Task StopAsync(CancellationToken token) + { + _stopping(); + return Task.CompletedTask; + } + + public void Dispose() => _disposing(); + } + + public class StartInstance : IDisposable + { + public int StopCalls { get; set; } + + public int DisposeCalls { get; set; } + + public void Stop() + { + StopCalls += 1; + } + + public void Dispose() + { + DisposeCalls += 1; + } + } + + public class FakeServer : IServer + { + public FakeServer() + { + Features = new FeatureCollection(); + Features.Set<IServerAddressesFeature>(new ServerAddressesFeature()); + } + + public IList<StartInstance> StartInstances { get; } = new List<StartInstance>(); + + public Func<IFeatureCollection> CreateRequestFeatures { get; set; } = NewFeatureCollection; + + public IFeatureCollection Features { get; } + + public static IFeatureCollection NewFeatureCollection() + { + var stub = new StubFeatures(); + var features = new FeatureCollection(); + features.Set<IHttpRequestFeature>(stub); + features.Set<IHttpResponseFeature>(stub); + return features; + } + + public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) + { + var startInstance = new StartInstance(); + StartInstances.Add(startInstance); + var context = application.CreateContext(CreateRequestFeatures()); + try + { + application.ProcessRequestAsync(context); + } + catch (Exception ex) + { + application.DisposeContext(context, ex); + throw; + } + application.DisposeContext(context, null); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + if (StartInstances != null) + { + foreach (var startInstance in StartInstances) + { + startInstance.Stop(); + } + } + + return Task.CompletedTask; + } + + public void Dispose() + { + if (StartInstances != null) + { + foreach (var startInstance in StartInstances) + { + startInstance.Dispose(); + } + } + } + } + + private class TestStartup : IStartup + { + public void Configure(IApplicationBuilder app) + { + throw new NotImplementedException(); + } + + public IServiceProvider ConfigureServices(IServiceCollection services) + { + throw new NotImplementedException(); + } + } + + private class ReadOnlyFeatureCollection : IFeatureCollection + { + public object this[Type key] + { + get { return null; } + set { throw new NotSupportedException(); } + } + + public bool IsReadOnly + { + get { return true; } + } + + public int Revision + { + get { return 0; } + } + + public void Dispose() + { + } + + public TFeature Get<TFeature>() + { + return default(TFeature); + } + + public IEnumerator<KeyValuePair<Type, object>> GetEnumerator() + { + yield break; + } + + public void Set<TFeature>(TFeature instance) + { + throw new NotSupportedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + yield break; + } + } + + private class AllMessagesAreNeeded : ILoggerProvider, ILogger + { + public bool IsEnabled(LogLevel logLevel) => true; + + public ILogger CreateLogger(string name) => this; + + public IDisposable BeginScope<TState>(TState state) + { + var stringified = state.ToString(); + return this; + } + public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) + { + var stringified = formatter(state, exception); + } + + public void Dispose() + { + } + } + + private class StubFeatures : IHttpRequestFeature, IHttpResponseFeature, IHeaderDictionary + { + public StubFeatures() + { + Headers = this; + Body = new MemoryStream(); + } + + public StringValues this[string key] + { + get { return StringValues.Empty; } + set { } + } + + public Stream Body { get; set; } + + public long? ContentLength { get; set; } + + public int Count => 0; + + public bool HasStarted { get; set; } + + public IHeaderDictionary Headers { get; set; } + + public bool IsReadOnly => false; + + public ICollection<string> Keys => null; + + public string Method { get; set; } + + public string Path { get; set; } + + public string PathBase { get; set; } + + public string Protocol { get; set; } + + public string QueryString { get; set; } + + public string RawTarget { get; set; } + + public string ReasonPhrase { get; set; } + + public string Scheme { get; set; } + + public int StatusCode { get; set; } + + public ICollection<StringValues> Values => null; + + public void Add(KeyValuePair<string, StringValues> item) { } + + public void Add(string key, StringValues value) { } + + public void Clear() { } + + public bool Contains(KeyValuePair<string, StringValues> item) => false; + + public bool ContainsKey(string key) => false; + + public void CopyTo(KeyValuePair<string, StringValues>[] array, int arrayIndex) { } + + public IEnumerator<KeyValuePair<string, StringValues>> GetEnumerator() => null; + + public void OnCompleted(Func<object, Task> callback, object state) { } + + public void OnStarting(Func<object, Task> callback, object state) { } + + public bool Remove(KeyValuePair<string, StringValues> item) => false; + + public bool Remove(string key) => false; + + public bool TryGetValue(string key, out StringValues value) + { + value = StringValues.Empty; + return false; + } + + IEnumerator IEnumerable.GetEnumerator() => null; + } + + private class StubHttpRequestIdentifierFeature : IHttpRequestIdentifierFeature + { + public string TraceIdentifier { get; set; } + } + } + + public static class TestServerWebHostExtensions + { + public static IWebHostBuilder UseFakeServer(this IWebHostBuilder builder) + { + return builder.ConfigureServices(services => services.AddSingleton<IServer, WebHostTests.FakeServer>()); + } + } +} \ No newline at end of file diff --git a/src/Hosting/Hosting/test/testroot/TextFile.txt b/src/Hosting/Hosting/test/testroot/TextFile.txt new file mode 100644 index 0000000000000000000000000000000000000000..d5669ad838619245bb3369435133d6bc2f6ae070 --- /dev/null +++ b/src/Hosting/Hosting/test/testroot/TextFile.txt @@ -0,0 +1 @@ +A text file. \ No newline at end of file diff --git a/src/Hosting/Hosting/test/testroot/wwwroot/README b/src/Hosting/Hosting/test/testroot/wwwroot/README new file mode 100644 index 0000000000000000000000000000000000000000..d3415c9f700e3ec63b9cfe1b78c90039167bc764 --- /dev/null +++ b/src/Hosting/Hosting/test/testroot/wwwroot/README @@ -0,0 +1 @@ +This file is here to keep directories needed by tests. Do not remove it. \ No newline at end of file diff --git a/src/Hosting/Server.Abstractions/src/Features/IServerAddressesFeature.cs b/src/Hosting/Server.Abstractions/src/Features/IServerAddressesFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..24a9a267a5cab61e854b37567e50ac8ba8571087 --- /dev/null +++ b/src/Hosting/Server.Abstractions/src/Features/IServerAddressesFeature.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Hosting.Server.Features +{ + public interface IServerAddressesFeature + { + ICollection<string> Addresses { get; } + + bool PreferHostingUrls { get; set; } + } +} diff --git a/src/Hosting/Server.Abstractions/src/IHttpApplication.cs b/src/Hosting/Server.Abstractions/src/IHttpApplication.cs new file mode 100644 index 0000000000000000000000000000000000000000..cc9d173da630b6b331e84a5cbf919b254fd6e156 --- /dev/null +++ b/src/Hosting/Server.Abstractions/src/IHttpApplication.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Hosting.Server +{ + /// <summary> + /// Represents an application. + /// </summary> + /// <typeparam name="TContext">The context associated with the application.</typeparam> + public interface IHttpApplication<TContext> + { + /// <summary> + /// Create a TContext given a collection of HTTP features. + /// </summary> + /// <param name="contextFeatures">A collection of HTTP features to be used for creating the TContext.</param> + /// <returns>The created TContext.</returns> + TContext CreateContext(IFeatureCollection contextFeatures); + + /// <summary> + /// Asynchronously processes an TContext. + /// </summary> + /// <param name="context">The TContext that the operation will process.</param> + Task ProcessRequestAsync(TContext context); + + /// <summary> + /// Dispose a given TContext. + /// </summary> + /// <param name="context">The TContext to be disposed.</param> + /// <param name="exception">The Exception thrown when processing did not complete successfully, otherwise null.</param> + void DisposeContext(TContext context, Exception exception); + } +} diff --git a/src/Hosting/Server.Abstractions/src/IServer.cs b/src/Hosting/Server.Abstractions/src/IServer.cs new file mode 100644 index 0000000000000000000000000000000000000000..b04e5a0d47d299f28111e87b1af4c2f7cdd29a03 --- /dev/null +++ b/src/Hosting/Server.Abstractions/src/IServer.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Hosting.Server +{ + /// <summary> + /// Represents a server. + /// </summary> + public interface IServer : IDisposable + { + /// <summary> + /// A collection of HTTP features of the server. + /// </summary> + IFeatureCollection Features { get; } + + /// <summary> + /// Start the server with an application. + /// </summary> + /// <param name="application">An instance of <see cref="IHttpApplication{TContext}"/>.</param> + /// <typeparam name="TContext">The context associated with the application.</typeparam> + /// <param name="cancellationToken">Indicates if the server startup should be aborted.</param> + Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken); + + /// <summary> + /// Stop processing requests and shut down the server, gracefully if possible. + /// </summary> + /// <param name="cancellationToken">Indicates if the graceful shutdown should be aborted.</param> + Task StopAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Hosting/Server.Abstractions/src/Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj b/src/Hosting/Server.Abstractions/src/Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj new file mode 100644 index 0000000000000000000000000000000000000000..13896a3b2d9a5c9dfbf3ee24f85cf35354eb9b6b --- /dev/null +++ b/src/Hosting/Server.Abstractions/src/Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj @@ -0,0 +1,16 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>ASP.NET Core hosting server abstractions for web applications.</Description> + <TargetFramework>netstandard2.0</TargetFramework> + <NoWarn>$(NoWarn);CS1591</NoWarn> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore;hosting</PackageTags> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Http.Features" /> + <Reference Include="Microsoft.Extensions.Configuration.Abstractions" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/Server.Abstractions/src/baseline.netcore.json b/src/Hosting/Server.Abstractions/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..30460913bd74cb6a8d1f9d73a6754bf6d40fef57 --- /dev/null +++ b/src/Hosting/Server.Abstractions/src/baseline.netcore.json @@ -0,0 +1,150 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Hosting.Server.Abstractions, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Hosting.Server.IHttpApplication<T0>", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CreateContext", + "Parameters": [ + { + "Name": "contextFeatures", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "ReturnType": "T0", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ProcessRequestAsync", + "Parameters": [ + { + "Name": "context", + "Type": "T0" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DisposeContext", + "Parameters": [ + { + "Name": "context", + "Type": "T0" + }, + { + "Name": "exception", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TContext", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Server.IServer", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "System.IDisposable" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Features", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StartAsync<T0>", + "Parameters": [ + { + "Name": "application", + "Type": "Microsoft.AspNetCore.Hosting.Server.IHttpApplication<T0>" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [ + { + "ParameterName": "TContext", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "StopAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.Server.Features.IServerAddressesFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Addresses", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection<System.String>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PreferHostingUrls", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PreferHostingUrls", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/ApplicationType.cs b/src/Hosting/Server.IntegrationTesting/src/Common/ApplicationType.cs new file mode 100644 index 0000000000000000000000000000000000000000..02d8715984c465c27eb0c11c3708a7065b34fcd9 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/ApplicationType.cs @@ -0,0 +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. + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public enum ApplicationType + { + Portable, + Standalone + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/DeploymentParameters.cs b/src/Hosting/Server.IntegrationTesting/src/Common/DeploymentParameters.cs new file mode 100644 index 0000000000000000000000000000000000000000..3d824741c37b9d1048ec15ba04867b2fda4c254e --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/DeploymentParameters.cs @@ -0,0 +1,145 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + /// <summary> + /// Parameters to control application deployment. + /// </summary> + public class DeploymentParameters + { + /// <summary> + /// Creates an instance of <see cref="DeploymentParameters"/>. + /// </summary> + /// <param name="applicationPath">Source code location of the target location to be deployed.</param> + /// <param name="serverType">Where to be deployed on.</param> + /// <param name="runtimeFlavor">Flavor of the clr to run against.</param> + /// <param name="runtimeArchitecture">Architecture of the runtime to be used.</param> + public DeploymentParameters( + string applicationPath, + ServerType serverType, + RuntimeFlavor runtimeFlavor, + RuntimeArchitecture runtimeArchitecture) + { + if (string.IsNullOrEmpty(applicationPath)) + { + throw new ArgumentException("Value cannot be null.", nameof(applicationPath)); + } + + if (!Directory.Exists(applicationPath)) + { + throw new DirectoryNotFoundException(string.Format("Application path {0} does not exist.", applicationPath)); + } + + if (runtimeArchitecture == RuntimeArchitecture.x86 && runtimeFlavor == RuntimeFlavor.CoreClr) + { + throw new NotSupportedException("32 bit deployment is not yet supported for CoreCLR. Don't remove the tests, just disable them for now."); + } + + ApplicationPath = applicationPath; + ApplicationName = new DirectoryInfo(ApplicationPath).Name; + ServerType = serverType; + RuntimeFlavor = runtimeFlavor; + EnvironmentVariables["ASPNETCORE_DETAILEDERRORS"] = "true"; + + var configAttribute = Assembly.GetCallingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>(); + if (configAttribute != null && !string.IsNullOrEmpty(configAttribute.Configuration)) + { + Configuration = configAttribute.Configuration; + } + } + + public ServerType ServerType { get; } + + public RuntimeFlavor RuntimeFlavor { get; } + + public RuntimeArchitecture RuntimeArchitecture { get; } = RuntimeArchitecture.x64; + + /// <summary> + /// Suggested base url for the deployed application. The final deployed url could be + /// different than this. Use <see cref="DeploymentResult.ApplicationBaseUri"/> for the + /// deployed url. + /// </summary> + public string ApplicationBaseUriHint { get; set; } + + public string EnvironmentName { get; set; } + + public string ServerConfigTemplateContent { get; set; } + + public string ServerConfigLocation { get; set; } + + public string SiteName { get; set; } + + public string ApplicationPath { get; } + + /// <summary> + /// Gets or sets the name of the application. This is used to execute the application when deployed. + /// Defaults to the file name of <see cref="ApplicationPath"/>. + /// </summary> + public string ApplicationName { get; set; } + + public string TargetFramework { get; set; } + + /// <summary> + /// Configuration under which to build (ex: Release or Debug) + /// </summary> + public string Configuration { get; set; } = "Debug"; + + /// <summary> + /// Space separated command line arguments to be passed to dotnet-publish + /// </summary> + public string AdditionalPublishParameters { get; set; } + + /// <summary> + /// Publish restores by default, this property opts out by default. + /// </summary> + public bool RestoreOnPublish { get; set; } + + /// <summary> + /// To publish the application before deployment. + /// </summary> + public bool PublishApplicationBeforeDeployment { get; set; } + + public bool PreservePublishedApplicationForDebugging { get; set; } = false; + + public bool StatusMessagesEnabled { get; set; } = true; + + public ApplicationType ApplicationType { get; set; } + + public string PublishedApplicationRootPath { get; set; } + + public HostingModel HostingModel { get; set; } + + /// <summary> + /// Environment variables to be set before starting the host. + /// Not applicable for IIS Scenarios. + /// </summary> + public IDictionary<string, string> EnvironmentVariables { get; } = new Dictionary<string, string>(); + + /// <summary> + /// Environment variables used when invoking dotnet publish. + /// </summary> + public IDictionary<string, string> PublishEnvironmentVariables { get; } = new Dictionary<string, string>(); + + /// <summary> + /// For any application level cleanup to be invoked after performing host cleanup. + /// </summary> + public Action<DeploymentParameters> UserAdditionalCleanup { get; set; } + + public override string ToString() + { + return string.Format( + "[Variation] :: ServerType={0}, Runtime={1}, Arch={2}, BaseUrlHint={3}, Publish={4}", + ServerType, + RuntimeFlavor, + RuntimeArchitecture, + ApplicationBaseUriHint, + PublishApplicationBeforeDeployment); + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/DeploymentResult.cs b/src/Hosting/Server.IntegrationTesting/src/Common/DeploymentResult.cs new file mode 100644 index 0000000000000000000000000000000000000000..6aed1f4d64aaa92f1ed506579c55ed19dcd126cc --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/DeploymentResult.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using System.Threading; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + /// <summary> + /// Result of a deployment. + /// </summary> + public class DeploymentResult + { + private readonly ILoggerFactory _loggerFactory; + + /// <summary> + /// Base Uri of the deployment application. + /// </summary> + public string ApplicationBaseUri { get; } + + /// <summary> + /// The folder where the application is hosted. This path can be different from the + /// original application source location if published before deployment. + /// </summary> + public string ContentRoot { get; } + + /// <summary> + /// Original deployment parameters used for this deployment. + /// </summary> + public DeploymentParameters DeploymentParameters { get; } + + /// <summary> + /// Triggered when the host process dies or pulled down. + /// </summary> + public CancellationToken HostShutdownToken { get; } + + /// <summary> + /// An <see cref="HttpClient"/> with <see cref="LoggingHandler"/> configured and the <see cref="HttpClient.BaseAddress"/> set to the <see cref="ApplicationBaseUri"/> + /// </summary> + public HttpClient HttpClient { get; } + + public DeploymentResult(ILoggerFactory loggerFactory, DeploymentParameters deploymentParameters, string applicationBaseUri) + : this(loggerFactory, deploymentParameters: deploymentParameters, applicationBaseUri: applicationBaseUri, contentRoot: string.Empty, hostShutdownToken: CancellationToken.None) + { } + + public DeploymentResult(ILoggerFactory loggerFactory, DeploymentParameters deploymentParameters, string applicationBaseUri, string contentRoot, CancellationToken hostShutdownToken) + { + _loggerFactory = loggerFactory; + + ApplicationBaseUri = applicationBaseUri; + ContentRoot = contentRoot; + DeploymentParameters = deploymentParameters; + HostShutdownToken = hostShutdownToken; + + HttpClient = CreateHttpClient(new HttpClientHandler()); + } + + /// <summary> + /// Create an <see cref="HttpClient"/> with <see cref="LoggingHandler"/> configured and the <see cref="HttpClient.BaseAddress"/> set to the <see cref="ApplicationBaseUri"/>, + /// but using the provided <see cref="HttpMessageHandler"/> and the underlying handler. + /// </summary> + /// <param name="baseHandler"></param> + /// <returns></returns> + public HttpClient CreateHttpClient(HttpMessageHandler baseHandler) => + new HttpClient(new LoggingHandler(_loggerFactory, baseHandler)) { BaseAddress = new Uri(ApplicationBaseUri) }; + } +} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/HostingModel.cs b/src/Hosting/Server.IntegrationTesting/src/Common/HostingModel.cs new file mode 100644 index 0000000000000000000000000000000000000000..1eb1aea8c0017b2e0f0d4a7d4f9364dfe671d381 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/HostingModel.cs @@ -0,0 +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. + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public enum HostingModel + { + OutOfProcess, + InProcess + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/LoggingHandler.cs b/src/Hosting/Server.IntegrationTesting/src/Common/LoggingHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..a1dc7e24dbd38bcfede738985c392bc3c81808ee --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/LoggingHandler.cs @@ -0,0 +1,34 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + internal class LoggingHandler : DelegatingHandler + { + private ILogger _logger; + + public LoggingHandler(ILoggerFactory loggerFactory, HttpMessageHandler innerHandler) : base(innerHandler) + { + _logger = loggerFactory.CreateLogger<HttpClient>(); + } + + protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + _logger.LogDebug("Sending {method} {url}", request.Method, request.RequestUri); + try + { + var response = await base.SendAsync(request, cancellationToken); + _logger.LogDebug("Received {statusCode} {reasonPhrase} {url}", response.StatusCode, response.ReasonPhrase, request.RequestUri); + return response; + } + catch (Exception ex) + { + _logger.LogError(0, ex, "Exception while sending '{method} {url}' : {exception}", request.Method, request.RequestUri, ex); + throw; + } + } + } +} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/ProcessLoggingExtensions.cs b/src/Hosting/Server.IntegrationTesting/src/Common/ProcessLoggingExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..8d7d20bc1e97e12bd5bf9c5a43955e2b53bd3d59 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/ProcessLoggingExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.Logging; + +namespace System.Diagnostics +{ + public static class ProcessLoggingExtensions + { + public static void StartAndCaptureOutAndErrToLogger(this Process process, string prefix, ILogger logger) + { + process.EnableRaisingEvents = true; + process.OutputDataReceived += (_, dataArgs) => + { + if (!string.IsNullOrEmpty(dataArgs.Data)) + { + logger.LogInformation($"{prefix} stdout: {{line}}", dataArgs.Data); + } + }; + + process.ErrorDataReceived += (_, dataArgs) => + { + if (!string.IsNullOrEmpty(dataArgs.Data)) + { + logger.LogWarning($"{prefix} stderr: {{line}}", dataArgs.Data); + } + }; + + process.Start(); + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/RetryHelper.cs b/src/Hosting/Server.IntegrationTesting/src/Common/RetryHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..75ac9f6f41041fae7054270ac7bfd708ed25c269 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/RetryHelper.cs @@ -0,0 +1,94 @@ +// 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.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class RetryHelper + { + /// <summary> + /// Retries every 1 sec for 60 times by default. + /// </summary> + /// <param name="retryBlock"></param> + /// <param name="logger"></param> + /// <param name="cancellationToken"></param> + /// <param name="retryCount"></param> + public static async Task<HttpResponseMessage> RetryRequest( + Func<Task<HttpResponseMessage>> retryBlock, + ILogger logger, + CancellationToken cancellationToken = default, + int retryCount = 60) + { + for (var retry = 0; retry < retryCount; retry++) + { + if (cancellationToken.IsCancellationRequested) + { + logger.LogInformation("Failed to connect, retry canceled."); + throw new OperationCanceledException("Failed to connect, retry canceled.", cancellationToken); + } + + try + { + logger.LogWarning("Retry count {retryCount}..", retry + 1); + var response = await retryBlock().ConfigureAwait(false); + + if (response.StatusCode == HttpStatusCode.ServiceUnavailable) + { + // Automatically retry on 503. May be application is still booting. + logger.LogWarning("Retrying a service unavailable error."); + continue; + } + + return response; // Went through successfully + } + catch (Exception exception) + { + if (retry == retryCount - 1) + { + logger.LogError(0, exception, "Failed to connect, retry limit exceeded."); + throw; + } + else + { + if (exception is HttpRequestException || exception is WebException) + { + logger.LogWarning("Failed to complete the request : {0}.", exception.Message); + await Task.Delay(1 * 1000); //Wait for a while before retry. + } + } + } + } + + logger.LogInformation("Failed to connect, retry limit exceeded."); + throw new OperationCanceledException("Failed to connect, retry limit exceeded."); + } + + public static void RetryOperation( + Action retryBlock, + Action<Exception> exceptionBlock, + int retryCount = 3, + int retryDelayMilliseconds = 0) + { + for (var retry = 0; retry < retryCount; ++retry) + { + try + { + retryBlock(); + break; + } + catch (Exception exception) + { + exceptionBlock(exception); + } + + Thread.Sleep(retryDelayMilliseconds); + } + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/RuntimeArchitecture.cs b/src/Hosting/Server.IntegrationTesting/src/Common/RuntimeArchitecture.cs new file mode 100644 index 0000000000000000000000000000000000000000..1e9c868b4bb03bda56ea5db092f387cce29422b3 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/RuntimeArchitecture.cs @@ -0,0 +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. + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public enum RuntimeArchitecture + { + x64, + x86 + } +} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/RuntimeFlavor.cs b/src/Hosting/Server.IntegrationTesting/src/Common/RuntimeFlavor.cs new file mode 100644 index 0000000000000000000000000000000000000000..510c713f59d5fc94853aea2160600aa2a966c997 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/RuntimeFlavor.cs @@ -0,0 +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. + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public enum RuntimeFlavor + { + Clr, + CoreClr + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/ServerType.cs b/src/Hosting/Server.IntegrationTesting/src/Common/ServerType.cs new file mode 100644 index 0000000000000000000000000000000000000000..060c8ed0cade861b774ca2e9b03ca3e4ad7e7938 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/ServerType.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public enum ServerType + { + IISExpress, + IIS, + WebListener, + Kestrel, + Nginx + } +} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/TestUriHelper.cs b/src/Hosting/Server.IntegrationTesting/src/Common/TestUriHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..7ebcb30367749d8b5680dd8702bad5dafe847ecd --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/TestUriHelper.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; +using System.Net; +using System.Net.Sockets; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common +{ + public static class TestUriHelper + { + public static Uri BuildTestUri() + { + return BuildTestUri(null); + } + + public static Uri BuildTestUri(string hint) + { + // If this method is called directly, there is no way to know the server type or whether status messages + // are enabled. It's safest to assume the server is WebListener (which doesn't support binding to dynamic + // port "0") and status messages are not enabled (so the assigned port cannot be scraped from console output). + return BuildTestUri(hint, serverType: ServerType.WebListener, statusMessagesEnabled: false); + } + + internal static Uri BuildTestUri(string hint, ServerType serverType, bool statusMessagesEnabled) + { + if (string.IsNullOrEmpty(hint)) + { + if (serverType == ServerType.Kestrel && statusMessagesEnabled) + { + // Most functional tests use this codepath and should directly bind to dynamic port "0" and scrape + // the assigned port from the status message, which should be 100% reliable since the port is bound + // once and never released. Binding to dynamic port "0" on "localhost" (both IPv4 and IPv6) is not + // supported, so the port is only bound on "127.0.0.1" (IPv4). If a test explicitly requires IPv6, + // it should provide a hint URL with "localhost" (IPv4 and IPv6) or "[::1]" (IPv6-only). + return new UriBuilder("http", "127.0.0.1", 0).Uri; + } + else + { + // If the server type is not Kestrel, or status messages are disabled, there is no status message + // from which to scrape the assigned port, so the less reliable GetNextPort() must be used. The + // port is bound on "localhost" (both IPv4 and IPv6), since this is supported when using a specific + // (non-zero) port. + return new UriBuilder("http", "localhost", GetNextPort()).Uri; + } + } + else + { + var uriHint = new Uri(hint); + if (uriHint.Port == 0) + { + // Only a few tests use this codepath, so it's fine to use the less reliable GetNextPort() for simplicity. + // The tests using this codepath will be reviewed to see if they can be changed to directly bind to dynamic + // port "0" on "127.0.0.1" and scrape the assigned port from the status message (the default codepath). + return new UriBuilder(uriHint) { Port = GetNextPort() }.Uri; + } + else + { + // If the hint contains a specific port, return it unchanged. + return uriHint; + } + } + } + + // Copied from https://github.com/aspnet/KestrelHttpServer/blob/47f1db20e063c2da75d9d89653fad4eafe24446c/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs#L508 + // + // This method is an attempt to safely get a free port from the OS. Most of the time, + // when binding to dynamic port "0" the OS increments the assigned port, so it's safe + // to re-use the assigned port in another process. However, occasionally the OS will reuse + // a recently assigned port instead of incrementing, which causes flaky tests with AddressInUse + // exceptions. This method should only be used when the application itself cannot use + // dynamic port "0" (e.g. IISExpress). Most functional tests using raw Kestrel + // (with status messages enabled) should directly bind to dynamic port "0" and scrape + // the assigned port from the status message, which should be 100% reliable since the port + // is bound once and never released. + public static int GetNextPort() + { + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + return ((IPEndPoint)socket.LocalEndPoint).Port; + } + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/ApplicationDeployer.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/ApplicationDeployer.cs new file mode 100644 index 0000000000000000000000000000000000000000..3c81f3290200f33bb95995c6cc05891ea555403d --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/ApplicationDeployer.cs @@ -0,0 +1,252 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + /// <summary> + /// Abstract base class of all deployers with implementation of some of the common helpers. + /// </summary> + public abstract class ApplicationDeployer : IApplicationDeployer + { + public static readonly string DotnetCommandName = "dotnet"; + + // This is the argument that separates the dotnet arguments for the args being passed to the + // app being run when running dotnet run + public static readonly string DotnetArgumentSeparator = "--"; + + private readonly Stopwatch _stopwatch = new Stopwatch(); + + public ApplicationDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) + { + DeploymentParameters = deploymentParameters; + LoggerFactory = loggerFactory; + Logger = LoggerFactory.CreateLogger(GetType().FullName); + } + + protected DeploymentParameters DeploymentParameters { get; } + + protected ILoggerFactory LoggerFactory { get; } + protected ILogger Logger { get; } + + public abstract Task<DeploymentResult> DeployAsync(); + + protected void DotnetPublish(string publishRoot = null) + { + using (Logger.BeginScope("dotnet-publish")) + { + if (string.IsNullOrEmpty(DeploymentParameters.TargetFramework)) + { + throw new Exception($"A target framework must be specified in the deployment parameters for applications that require publishing before deployment"); + } + + DeploymentParameters.PublishedApplicationRootPath = publishRoot ?? Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + + var parameters = $"publish " + + $" --output \"{DeploymentParameters.PublishedApplicationRootPath}\"" + + $" --framework {DeploymentParameters.TargetFramework}" + + $" --configuration {DeploymentParameters.Configuration}" + + (DeploymentParameters.RestoreOnPublish + ? string.Empty + : " --no-restore -p:VerifyMatchingImplicitPackageVersion=false"); + // Set VerifyMatchingImplicitPackageVersion to disable errors when Microsoft.NETCore.App's version is overridden externally + // This verification doesn't matter if we are skipping restore during tests. + + if (DeploymentParameters.ApplicationType == ApplicationType.Standalone) + { + parameters += $" --runtime {GetRuntimeIdentifier()}"; + } + + parameters += $" {DeploymentParameters.AdditionalPublishParameters}"; + + var startInfo = new ProcessStartInfo + { + FileName = DotnetCommandName, + Arguments = parameters, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + WorkingDirectory = DeploymentParameters.ApplicationPath, + }; + + AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.PublishEnvironmentVariables); + + var hostProcess = new Process() { StartInfo = startInfo }; + + Logger.LogInformation($"Executing command {DotnetCommandName} {parameters}"); + + hostProcess.StartAndCaptureOutAndErrToLogger("dotnet-publish", Logger); + + hostProcess.WaitForExit(); + + if (hostProcess.ExitCode != 0) + { + var message = $"{DotnetCommandName} publish exited with exit code : {hostProcess.ExitCode}"; + Logger.LogError(message); + throw new Exception(message); + } + + Logger.LogInformation($"{DotnetCommandName} publish finished with exit code : {hostProcess.ExitCode}"); + } + } + + protected void CleanPublishedOutput() + { + using (Logger.BeginScope("CleanPublishedOutput")) + { + if (DeploymentParameters.PreservePublishedApplicationForDebugging) + { + Logger.LogWarning( + "Skipping deleting the locally published folder as property " + + $"'{nameof(DeploymentParameters.PreservePublishedApplicationForDebugging)}' is set to 'true'."); + } + else + { + RetryHelper.RetryOperation( + () => Directory.Delete(DeploymentParameters.PublishedApplicationRootPath, true), + e => Logger.LogWarning($"Failed to delete directory : {e.Message}"), + retryCount: 3, + retryDelayMilliseconds: 100); + } + } + } + + protected void ShutDownIfAnyHostProcess(Process hostProcess) + { + if (hostProcess != null && !hostProcess.HasExited) + { + Logger.LogInformation("Attempting to cancel process {0}", hostProcess.Id); + + // Shutdown the host process. + hostProcess.KillTree(); + if (!hostProcess.HasExited) + { + Logger.LogWarning("Unable to terminate the host process with process Id '{processId}", hostProcess.Id); + } + else + { + Logger.LogInformation("Successfully terminated host process with process Id '{processId}'", hostProcess.Id); + } + } + else + { + Logger.LogWarning("Host process already exited or never started successfully."); + } + } + + protected void AddEnvironmentVariablesToProcess(ProcessStartInfo startInfo, IDictionary<string, string> environmentVariables) + { + var environment = startInfo.Environment; + SetEnvironmentVariable(environment, "ASPNETCORE_ENVIRONMENT", DeploymentParameters.EnvironmentName); + + foreach (var environmentVariable in environmentVariables) + { + SetEnvironmentVariable(environment, environmentVariable.Key, environmentVariable.Value); + } + } + + protected void SetEnvironmentVariable(IDictionary<string, string> environment, string name, string value) + { + if (value == null) + { + Logger.LogInformation("Removing environment variable {name}", name); + environment.Remove(name); + } + else + { + Logger.LogInformation("SET {name}={value}", name, value); + environment[name] = value; + } + } + + protected void InvokeUserApplicationCleanup() + { + using (Logger.BeginScope("UserAdditionalCleanup")) + { + if (DeploymentParameters.UserAdditionalCleanup != null) + { + // User cleanup. + try + { + DeploymentParameters.UserAdditionalCleanup(DeploymentParameters); + } + catch (Exception exception) + { + Logger.LogWarning("User cleanup code failed with exception : {exception}", exception.Message); + } + } + } + } + + protected void TriggerHostShutdown(CancellationTokenSource hostShutdownSource) + { + Logger.LogInformation("Host process shutting down."); + try + { + hostShutdownSource.Cancel(); + } + catch (Exception) + { + // Suppress errors. + } + } + + protected void StartTimer() + { + Logger.LogInformation($"Deploying {DeploymentParameters.ToString()}"); + _stopwatch.Start(); + } + + protected void StopTimer() + { + _stopwatch.Stop(); + Logger.LogInformation("[Time]: Total time taken for this test variation '{t}' seconds", _stopwatch.Elapsed.TotalSeconds); + } + + public abstract void Dispose(); + + private string GetRuntimeIdentifier() + { + var architecture = GetArchitecture(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "win7-" + architecture; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return "linux-" + architecture; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "osx-" + architecture; + } + else + { + throw new InvalidOperationException("Unrecognized operation system platform"); + } + } + + private string GetArchitecture() + { + switch (RuntimeInformation.OSArchitecture) + { + case Architecture.X86: + return "x86"; + case Architecture.X64: + return "x64"; + default: + throw new NotSupportedException($"Unsupported architecture: {RuntimeInformation.OSArchitecture}"); + } + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/ApplicationDeployerFactory.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/ApplicationDeployerFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..eb66761807a75c5590b3f057b3046bed01d093bd --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/ApplicationDeployerFactory.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + /// <summary> + /// Factory to create an appropriate deployer based on <see cref="DeploymentParameters"/>. + /// </summary> + public class ApplicationDeployerFactory + { + /// <summary> + /// Creates a deployer instance based on settings in <see cref="DeploymentParameters"/>. + /// </summary> + /// <param name="deploymentParameters"></param> + /// <param name="loggerFactory"></param> + /// <returns></returns> + public static IApplicationDeployer Create(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) + { + if (deploymentParameters == null) + { + throw new ArgumentNullException(nameof(deploymentParameters)); + } + + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + switch (deploymentParameters.ServerType) + { + case ServerType.IISExpress: + return new IISExpressDeployer(deploymentParameters, loggerFactory); + case ServerType.IIS: + throw new NotSupportedException("The IIS deployer is no longer supported"); + case ServerType.WebListener: + case ServerType.Kestrel: + return new SelfHostDeployer(deploymentParameters, loggerFactory); + case ServerType.Nginx: + return new NginxDeployer(deploymentParameters, loggerFactory); + default: + throw new NotSupportedException( + string.Format("Found no deployers suitable for server type '{0}' with the current runtime.", + deploymentParameters.ServerType) + ); + } + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/IApplicationDeployer.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/IApplicationDeployer.cs new file mode 100644 index 0000000000000000000000000000000000000000..400ae978edcf70839eae1ed75786dbb2ca9e1652 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/IApplicationDeployer.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + /// <summary> + /// Common operations on an application deployer. + /// </summary> + public interface IApplicationDeployer : IDisposable + { + /// <summary> + /// Deploys the application to the target with specified <see cref="DeploymentParameters"/>. + /// </summary> + /// <returns></returns> + Task<DeploymentResult> DeployAsync(); + } +} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/IISExpressDeployer.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/IISExpressDeployer.cs new file mode 100644 index 0000000000000000000000000000000000000000..bc7aecb70096b3a016398be56c48c6344167ac3e --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/IISExpressDeployer.cs @@ -0,0 +1,317 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + /// <summary> + /// Deployment helper for IISExpress. + /// </summary> + public class IISExpressDeployer : ApplicationDeployer + { + private const string IISExpressRunningMessage = "IIS Express is running."; + private const string FailedToInitializeBindingsMessage = "Failed to initialize site bindings"; + private const string UnableToStartIISExpressMessage = "Unable to start iisexpress."; + private const int MaximumAttempts = 5; + + private static readonly Regex UrlDetectorRegex = new Regex(@"^\s*Successfully registered URL ""(?<url>[^""]+)"" for site.*$"); + + private Process _hostProcess; + + public IISExpressDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) + : base(deploymentParameters, loggerFactory) + { + } + + public bool IsWin8OrLater + { + get + { + var win8Version = new Version(6, 2); + + return (Environment.OSVersion.Version >= win8Version); + } + } + + public bool Is64BitHost + { + get + { + return Environment.Is64BitOperatingSystem; + } + } + + public override async Task<DeploymentResult> DeployAsync() + { + using (Logger.BeginScope("Deployment")) + { + // Start timer + StartTimer(); + + // For now we always auto-publish. Otherwise we'll have to write our own local web.config for the HttpPlatformHandler + DeploymentParameters.PublishApplicationBeforeDeployment = true; + if (DeploymentParameters.PublishApplicationBeforeDeployment) + { + DotnetPublish(); + } + + var contentRoot = DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath; + + var testUri = TestUriHelper.BuildTestUri(DeploymentParameters.ApplicationBaseUriHint); + + // Launch the host process. + var (actualUri, hostExitToken) = await StartIISExpressAsync(testUri, contentRoot); + + Logger.LogInformation("Application ready at URL: {appUrl}", actualUri); + + // Right now this works only for urls like http://localhost:5001/. Does not work for http://localhost:5001/subpath. + return new DeploymentResult( + LoggerFactory, + DeploymentParameters, + applicationBaseUri: actualUri.ToString(), + contentRoot: contentRoot, + hostShutdownToken: hostExitToken); + } + } + + private async Task<(Uri url, CancellationToken hostExitToken)> StartIISExpressAsync(Uri uri, string contentRoot) + { + using (Logger.BeginScope("StartIISExpress")) + { + var port = uri.Port; + if (port == 0) + { + port = TestUriHelper.GetNextPort(); + } + + for (var attempt = 0; attempt < MaximumAttempts; attempt++) + { + Logger.LogInformation("Attempting to start IIS Express on port: {port}", port); + + if (!string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigTemplateContent)) + { + var serverConfig = DeploymentParameters.ServerConfigTemplateContent; + + // Pass on the applicationhost.config to iis express. With this don't need to pass in the /path /port switches as they are in the applicationHost.config + // We take a copy of the original specified applicationHost.Config to prevent modifying the one in the repo. + + if (serverConfig.Contains("[ANCMPath]")) + { + // We need to pick the bitness based the OS / IIS Express, not the application. + // We'll eventually add support for choosing which IIS Express bitness to run: https://github.com/aspnet/Hosting/issues/880 + var ancmFile = Path.Combine(contentRoot, Is64BitHost ? @"x64\aspnetcore.dll" : @"x86\aspnetcore.dll"); + // Bin deployed by Microsoft.AspNetCore.AspNetCoreModule.nupkg + + if (!File.Exists(Environment.ExpandEnvironmentVariables(ancmFile))) + { + throw new FileNotFoundException("AspNetCoreModule could not be found.", ancmFile); + } + + Logger.LogDebug("Writing ANCMPath '{ancmPath}' to config", ancmFile); + serverConfig = + serverConfig.Replace("[ANCMPath]", ancmFile); + } + + Logger.LogDebug("Writing ApplicationPhysicalPath '{applicationPhysicalPath}' to config", contentRoot); + Logger.LogDebug("Writing Port '{port}' to config", port); + serverConfig = + serverConfig + .Replace("[ApplicationPhysicalPath]", contentRoot) + .Replace("[PORT]", port.ToString()); + + DeploymentParameters.ServerConfigLocation = Path.GetTempFileName(); + + if (serverConfig.Contains("[HostingModel]")) + { + var hostingModel = DeploymentParameters.HostingModel.ToString(); + serverConfig.Replace("[HostingModel]", hostingModel); + Logger.LogDebug("Writing HostingModel '{hostingModel}' to config", hostingModel); + } + + Logger.LogDebug("Saving Config to {configPath}", DeploymentParameters.ServerConfigLocation); + + if (Logger.IsEnabled(LogLevel.Trace)) + { + Logger.LogTrace($"Config File Content:{Environment.NewLine}===START CONFIG==={Environment.NewLine}{{configContent}}{Environment.NewLine}===END CONFIG===", serverConfig); + } + + File.WriteAllText(DeploymentParameters.ServerConfigLocation, serverConfig); + } + + if (DeploymentParameters.HostingModel == HostingModel.InProcess) + { + ModifyWebConfigToInProcess(); + } + + var parameters = string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigLocation) ? + string.Format("/port:{0} /path:\"{1}\" /trace:error", uri.Port, contentRoot) : + string.Format("/site:{0} /config:{1} /trace:error", DeploymentParameters.SiteName, DeploymentParameters.ServerConfigLocation); + + var iisExpressPath = GetIISExpressPath(); + + Logger.LogInformation("Executing command : {iisExpress} {parameters}", iisExpressPath, parameters); + + var startInfo = new ProcessStartInfo + { + FileName = iisExpressPath, + Arguments = parameters, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true + }; + + AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables); + + Uri url = null; + var started = new TaskCompletionSource<bool>(); + + var process = new Process() { StartInfo = startInfo }; + process.OutputDataReceived += (sender, dataArgs) => + { + if (string.Equals(dataArgs.Data, UnableToStartIISExpressMessage)) + { + // We completely failed to start and we don't really know why + started.TrySetException(new InvalidOperationException("Failed to start IIS Express")); + } + else if (string.Equals(dataArgs.Data, FailedToInitializeBindingsMessage)) + { + started.TrySetResult(false); + } + else if (string.Equals(dataArgs.Data, IISExpressRunningMessage)) + { + started.TrySetResult(true); + } + else if (!string.IsNullOrEmpty(dataArgs.Data)) + { + var m = UrlDetectorRegex.Match(dataArgs.Data); + if (m.Success) + { + url = new Uri(m.Groups["url"].Value); + } + } + }; + + process.EnableRaisingEvents = true; + var hostExitTokenSource = new CancellationTokenSource(); + process.Exited += (sender, e) => + { + Logger.LogInformation("iisexpress Process {pid} shut down", process.Id); + + // If TrySetResult was called above, this will just silently fail to set the new state, which is what we want + started.TrySetException(new Exception($"Command exited unexpectedly with exit code: {process.ExitCode}")); + + TriggerHostShutdown(hostExitTokenSource); + }; + process.StartAndCaptureOutAndErrToLogger("iisexpress", Logger); + Logger.LogInformation("iisexpress Process {pid} started", process.Id); + + if (process.HasExited) + { + Logger.LogError("Host process {processName} {pid} exited with code {exitCode} or failed to start.", startInfo.FileName, process.Id, process.ExitCode); + throw new Exception("Failed to start host"); + } + + // Wait for the app to start + // The timeout here is large, because we don't know how long the test could need + // We cover a lot of error cases above, but I want to make sure we eventually give up and don't hang the build + // just in case we missed one -anurse + if (!await started.Task.TimeoutAfter(TimeSpan.FromMinutes(10))) + { + Logger.LogInformation("iisexpress Process {pid} failed to bind to port {port}, trying again", _hostProcess.Id, port); + + // Wait for the process to exit and try again + process.WaitForExit(30 * 1000); + await Task.Delay(1000); // Wait a second to make sure the socket is completely cleaned up + } + else + { + _hostProcess = process; + Logger.LogInformation("Started iisexpress successfully. Process Id : {processId}, Port: {port}", _hostProcess.Id, port); + return (url: url, hostExitToken: hostExitTokenSource.Token); + } + } + + var message = $"Failed to initialize IIS Express after {MaximumAttempts} attempts to select a port"; + Logger.LogError(message); + throw new TimeoutException(message); + } + } + + private string GetIISExpressPath() + { + // Get path to program files + var iisExpressPath = Path.Combine(Environment.GetEnvironmentVariable("SystemDrive") + "\\", "Program Files", "IIS Express", "iisexpress.exe"); + + if (!File.Exists(iisExpressPath)) + { + throw new Exception("Unable to find IISExpress on the machine: " + iisExpressPath); + } + + return iisExpressPath; + } + + public override void Dispose() + { + using (Logger.BeginScope("Dispose")) + { + ShutDownIfAnyHostProcess(_hostProcess); + + if (!string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigLocation) + && File.Exists(DeploymentParameters.ServerConfigLocation)) + { + // Delete the temp applicationHostConfig that we created. + Logger.LogDebug("Deleting applicationHost.config file from {configLocation}", DeploymentParameters.ServerConfigLocation); + try + { + File.Delete(DeploymentParameters.ServerConfigLocation); + } + catch (Exception exception) + { + // Ignore delete failures - just write a log. + Logger.LogWarning("Failed to delete '{config}'. Exception : {exception}", DeploymentParameters.ServerConfigLocation, exception.Message); + } + } + + if (DeploymentParameters.PublishApplicationBeforeDeployment) + { + CleanPublishedOutput(); + } + + InvokeUserApplicationCleanup(); + + StopTimer(); + } + + // If by this point, the host process is still running (somehow), throw an error. + // A test failure is better than a silent hang and unknown failure later on + if (_hostProcess != null && !_hostProcess.HasExited) + { + throw new Exception($"iisexpress Process {_hostProcess.Id} failed to shutdown"); + } + } + + // Transforms the web.config file to include the hostingModel="inprocess" element + // and adds the server type = Microsoft.AspNetServer.IIS such that Kestrel isn't added again in ServerTests + private void ModifyWebConfigToInProcess() + { + var webConfigFile = $"{DeploymentParameters.PublishedApplicationRootPath}/web.config"; + var config = XDocument.Load(webConfigFile); + var element = config.Descendants("aspNetCore").FirstOrDefault(); + element.SetAttributeValue("hostingModel", "inprocess"); + config.Save(webConfigFile); + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/NginxDeployer.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/NginxDeployer.cs new file mode 100644 index 0000000000000000000000000000000000000000..1bc6c4b766ad90ce3d52cca11a9beca56983f9df --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/NginxDeployer.cs @@ -0,0 +1,165 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + /// <summary> + /// Deployer for Kestrel on Nginx. + /// </summary> + public class NginxDeployer : SelfHostDeployer + { + private string _configFile; + private readonly int _waitTime = (int)TimeSpan.FromSeconds(30).TotalMilliseconds; + + public NginxDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) + : base(deploymentParameters, loggerFactory) + { + } + + public override async Task<DeploymentResult> DeployAsync() + { + using (Logger.BeginScope("Deploy")) + { + _configFile = Path.GetTempFileName(); + var uri = string.IsNullOrEmpty(DeploymentParameters.ApplicationBaseUriHint) ? + TestUriHelper.BuildTestUri() : + new Uri(DeploymentParameters.ApplicationBaseUriHint); + + var redirectUri = TestUriHelper.BuildTestUri(); + + if (DeploymentParameters.PublishApplicationBeforeDeployment) + { + DotnetPublish(); + } + + var (appUri, exitToken) = await StartSelfHostAsync(redirectUri); + + SetupNginx(appUri.ToString(), uri); + + Logger.LogInformation("Application ready at URL: {appUrl}", uri); + + // Wait for App to be loaded since Nginx returns 502 instead of 503 when App isn't loaded + // Target actual address to avoid going through Nginx proxy + using (var httpClient = new HttpClient()) + { + var response = await RetryHelper.RetryRequest(() => + { + return httpClient.GetAsync(redirectUri); + }, Logger, exitToken); + + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException("Deploy failed"); + } + } + + return new DeploymentResult( + LoggerFactory, + DeploymentParameters, + applicationBaseUri: uri.ToString(), + contentRoot: DeploymentParameters.ApplicationPath, + hostShutdownToken: exitToken); + } + } + + private void SetupNginx(string redirectUri, Uri originalUri) + { + using (Logger.BeginScope("SetupNginx")) + { + // copy nginx.conf template and replace pertinent information + var pidFile = Path.Combine(DeploymentParameters.ApplicationPath, $"{Guid.NewGuid()}.nginx.pid"); + var errorLog = Path.Combine(DeploymentParameters.ApplicationPath, "nginx.error.log"); + var accessLog = Path.Combine(DeploymentParameters.ApplicationPath, "nginx.access.log"); + DeploymentParameters.ServerConfigTemplateContent = DeploymentParameters.ServerConfigTemplateContent + .Replace("[user]", Environment.GetEnvironmentVariable("LOGNAME")) + .Replace("[errorlog]", errorLog) + .Replace("[accesslog]", accessLog) + .Replace("[listenPort]", originalUri.Port.ToString()) + .Replace("[redirectUri]", redirectUri) + .Replace("[pidFile]", pidFile); + Logger.LogDebug("Using PID file: {pidFile}", pidFile); + Logger.LogDebug("Using Error Log file: {errorLog}", pidFile); + Logger.LogDebug("Using Access Log file: {accessLog}", pidFile); + if (Logger.IsEnabled(LogLevel.Trace)) + { + Logger.LogTrace($"Config File Content:{Environment.NewLine}===START CONFIG==={Environment.NewLine}{{configContent}}{Environment.NewLine}===END CONFIG===", DeploymentParameters.ServerConfigTemplateContent); + } + File.WriteAllText(_configFile, DeploymentParameters.ServerConfigTemplateContent); + + var startInfo = new ProcessStartInfo + { + FileName = "nginx", + Arguments = $"-c {_configFile}", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + // Trying a work around for https://github.com/aspnet/Hosting/issues/140. + RedirectStandardInput = true + }; + + using (var runNginx = new Process() { StartInfo = startInfo }) + { + runNginx.StartAndCaptureOutAndErrToLogger("nginx start", Logger); + runNginx.WaitForExit(_waitTime); + if (runNginx.ExitCode != 0) + { + throw new Exception("Failed to start nginx"); + } + + // Read the PID file + if(!File.Exists(pidFile)) + { + Logger.LogWarning("Unable to find nginx PID file: {pidFile}", pidFile); + } + else + { + var pid = File.ReadAllText(pidFile); + Logger.LogInformation("nginx process ID {pid} started", pid); + } + } + } + } + + public override void Dispose() + { + using (Logger.BeginScope("Dispose")) + { + if (File.Exists(_configFile)) + { + var startInfo = new ProcessStartInfo + { + FileName = "nginx", + Arguments = $"-s stop -c {_configFile}", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + // Trying a work around for https://github.com/aspnet/Hosting/issues/140. + RedirectStandardInput = true + }; + + using (var runNginx = new Process() { StartInfo = startInfo }) + { + runNginx.StartAndCaptureOutAndErrToLogger("nginx stop", Logger); + runNginx.WaitForExit(_waitTime); + Logger.LogInformation("nginx stop command issued"); + } + + Logger.LogDebug("Deleting config file: {configFile}", _configFile); + File.Delete(_configFile); + } + + base.Dispose(); + } + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/RemotePSSessionHelper.ps1 b/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/RemotePSSessionHelper.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..e5c54d21e810eed78d5cb9327383854abe4bdde8 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/RemotePSSessionHelper.ps1 @@ -0,0 +1,67 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory=$true)] + [string]$serverName, + + [Parameter(Mandatory=$true)] + [string]$accountName, + + [Parameter(Mandatory=$true)] + [string]$accountPassword, + + [Parameter(Mandatory=$true)] + [string]$deployedFolderPath, + + [Parameter(Mandatory=$false)] + [string]$dotnetRuntimePath = "", + + [Parameter(Mandatory=$true)] + [string]$executablePath, + + [Parameter(Mandatory=$false)] + [string]$executableParameters, + + [Parameter(Mandatory=$true)] + [string]$serverType, + + [Parameter(Mandatory=$true)] + [string]$serverAction, + + [Parameter(Mandatory=$true)] + [string]$applicationBaseUrl, + + [Parameter(Mandatory=$false)] + [string]$environmentVariables +) + +Write-Host "`nExecuting deployment helper script on machine '$serverName'" +Write-Host "`nStarting a powershell session to machine '$serverName'" + +$securePassword = ConvertTo-SecureString $accountPassword -AsPlainText -Force +$credentials= New-Object System.Management.Automation.PSCredential ($accountName, $securePassword) +$psSession = New-PSSession -ComputerName $serverName -credential $credentials + +$remoteResult="0" +if ($serverAction -eq "StartServer") +{ + Write-Host "Starting the application on machine '$serverName'" + $startServerScriptPath = "$PSScriptRoot\StartServer.ps1" + $remoteResult=Invoke-Command -Session $psSession -FilePath $startServerScriptPath -ArgumentList $deployedFolderPath, $dotnetRuntimePath, $executablePath, $executableParameters, $serverType, $serverName, $applicationBaseUrl, $environmentVariables +} +else +{ + Write-Host "Stopping the application on machine '$serverName'" + $stopServerScriptPath = "$PSScriptRoot\StopServer.ps1" + $serverProcessName = [System.IO.Path]::GetFileNameWithoutExtension($executablePath) + $remoteResult=Invoke-Command -Session $psSession -FilePath $stopServerScriptPath -ArgumentList $deployedFolderPath, $serverProcessName, $serverType, $serverName +} + +Remove-PSSession $psSession + +# NOTE: Currenty there is no straight forward way to get the exit code from a remotely executing session, so +# we print out the exit code in the remote script and capture it's output to get the exit code. +if($remoteResult.Length > 0) +{ + $finalExitCode=$remoteResult[$remoteResult.Length-1] + exit $finalExitCode +} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/RemoteWindowsDeployer.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/RemoteWindowsDeployer.cs new file mode 100644 index 0000000000000000000000000000000000000000..a102cd02da371fa6d6f8a0c2d2098c9dde01bcf2 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/RemoteWindowsDeployer.cs @@ -0,0 +1,361 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class RemoteWindowsDeployer : ApplicationDeployer + { + /// <summary> + /// Example: If the share path is '\\dir1\dir2', then this returns the full path to the + /// deployed folder. Example: '\\dir1\dir2\048f6c99-de3e-488a-8020-f9eb277818d9' + /// </summary> + private string _deployedFolderPathInFileShare; + private readonly RemoteWindowsDeploymentParameters _deploymentParameters; + private bool _isDisposed; + private static readonly Lazy<Scripts> _scripts = new Lazy<Scripts>(() => CopyEmbeddedScriptFilesToDisk()); + + public RemoteWindowsDeployer(RemoteWindowsDeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) + : base(deploymentParameters, loggerFactory) + { + _deploymentParameters = deploymentParameters; + + if (_deploymentParameters.ServerType != ServerType.IIS + && _deploymentParameters.ServerType != ServerType.Kestrel + && _deploymentParameters.ServerType != ServerType.WebListener) + { + throw new InvalidOperationException($"Server type {_deploymentParameters.ServerType} is not supported for remote deployment." + + $" Supported server types are {nameof(ServerType.Kestrel)}, {nameof(ServerType.IIS)} and {nameof(ServerType.WebListener)}"); + } + + if (string.IsNullOrWhiteSpace(_deploymentParameters.ServerName)) + { + throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerName}' for {nameof(RemoteWindowsDeploymentParameters.ServerName)}"); + } + + if (string.IsNullOrWhiteSpace(_deploymentParameters.ServerAccountName)) + { + throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerAccountName}' for {nameof(RemoteWindowsDeploymentParameters.ServerAccountName)}." + + " Account credentials are required to enable creating a powershell session to the remote server."); + } + + if (string.IsNullOrWhiteSpace(_deploymentParameters.ServerAccountPassword)) + { + throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerAccountPassword}' for {nameof(RemoteWindowsDeploymentParameters.ServerAccountPassword)}." + + " Account credentials are required to enable creating a powershell session to the remote server."); + } + + if (_deploymentParameters.ApplicationType == ApplicationType.Portable + && string.IsNullOrWhiteSpace(_deploymentParameters.DotnetRuntimePath)) + { + throw new ArgumentException($"Invalid value '{_deploymentParameters.DotnetRuntimePath}' for {nameof(RemoteWindowsDeploymentParameters.DotnetRuntimePath)}. " + + "It must be non-empty for portable apps."); + } + + if (string.IsNullOrWhiteSpace(_deploymentParameters.RemoteServerFileSharePath)) + { + throw new ArgumentException($"Invalid value for {nameof(RemoteWindowsDeploymentParameters.RemoteServerFileSharePath)}." + + " . A file share is required to copy the application's published output."); + } + + if (string.IsNullOrWhiteSpace(_deploymentParameters.ApplicationBaseUriHint)) + { + throw new ArgumentException($"Invalid value for {nameof(RemoteWindowsDeploymentParameters.ApplicationBaseUriHint)}."); + } + } + + public override async Task<DeploymentResult> DeployAsync() + { + using (Logger.BeginScope("Deploy")) + { + if (_isDisposed) + { + throw new ObjectDisposedException("This instance of deployer has already been disposed."); + } + + // Publish the app to a local temp folder on the machine where the test is running + DotnetPublish(); + + if (_deploymentParameters.ServerType == ServerType.IIS) + { + UpdateWebConfig(); + } + + var folderId = Guid.NewGuid().ToString(); + _deployedFolderPathInFileShare = Path.Combine(_deploymentParameters.RemoteServerFileSharePath, folderId); + + DirectoryCopy( + _deploymentParameters.PublishedApplicationRootPath, + _deployedFolderPathInFileShare, + copySubDirs: true); + Logger.LogInformation($"Copied the locally published folder to the file share path '{_deployedFolderPathInFileShare}'"); + + await RunScriptAsync("StartServer"); + + return new DeploymentResult( + LoggerFactory, + DeploymentParameters, + DeploymentParameters.ApplicationBaseUriHint); + } + } + + public override void Dispose() + { + using (Logger.BeginScope("Dispose")) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + try + { + Logger.LogInformation($"Stopping the application on the server '{_deploymentParameters.ServerName}'"); + RunScriptAsync("StopServer").Wait(); + } + catch (Exception ex) + { + Logger.LogWarning(0, "Failed to stop the server.", ex); + } + + try + { + Logger.LogInformation($"Deleting the deployed folder '{_deployedFolderPathInFileShare}'"); + Directory.Delete(_deployedFolderPathInFileShare, recursive: true); + } + catch (Exception ex) + { + Logger.LogWarning(0, $"Failed to delete the deployed folder '{_deployedFolderPathInFileShare}'.", ex); + } + + try + { + Logger.LogInformation($"Deleting the locally published folder '{DeploymentParameters.PublishedApplicationRootPath}'"); + Directory.Delete(DeploymentParameters.PublishedApplicationRootPath, recursive: true); + } + catch (Exception ex) + { + Logger.LogWarning(0, $"Failed to delete the locally published folder '{DeploymentParameters.PublishedApplicationRootPath}'.", ex); + } + } + } + + private void UpdateWebConfig() + { + var webConfigFilePath = Path.Combine(_deploymentParameters.PublishedApplicationRootPath, "web.config"); + var webConfig = XDocument.Load(webConfigFilePath); + var aspNetCoreSection = webConfig.Descendants("aspNetCore") + .Single(); + + // if the dotnet runtime path is specified, update the published web.config file to have that path + if (!string.IsNullOrEmpty(_deploymentParameters.DotnetRuntimePath)) + { + aspNetCoreSection.SetAttributeValue( + "processPath", + Path.Combine(_deploymentParameters.DotnetRuntimePath, "dotnet.exe")); + } + + var environmentVariablesSection = aspNetCoreSection.Elements("environmentVariables").FirstOrDefault(); + if (environmentVariablesSection == null) + { + environmentVariablesSection = new XElement("environmentVariables"); + aspNetCoreSection.Add(environmentVariablesSection); + } + + foreach (var envVariablePair in _deploymentParameters.EnvironmentVariables) + { + var environmentVariable = new XElement("environmentVariable"); + environmentVariable.SetAttributeValue("name", envVariablePair.Key); + environmentVariable.SetAttributeValue("value", envVariablePair.Value); + environmentVariablesSection.Add(environmentVariable); + } + + if(Logger.IsEnabled(LogLevel.Trace)) + { + Logger.LogTrace($"Config File Content:{Environment.NewLine}===START CONFIG==={Environment.NewLine}{{configContent}}{Environment.NewLine}===END CONFIG===", webConfig.ToString()); + } + + using (var fileStream = File.Open(webConfigFilePath, FileMode.Open)) + { + webConfig.Save(fileStream); + } + } + + private async Task RunScriptAsync(string serverAction) + { + using (Logger.BeginScope($"RunScript:{serverAction}")) + { + var remotePSSessionHelperScript = _scripts.Value.RemotePSSessionHelper; + + string executablePath = null; + string executableParameters = null; + var applicationName = new DirectoryInfo(DeploymentParameters.ApplicationPath).Name; + if (DeploymentParameters.ApplicationType == ApplicationType.Portable) + { + executablePath = "dotnet.exe"; + executableParameters = Path.Combine(_deployedFolderPathInFileShare, applicationName + ".dll"); + } + else + { + executablePath = Path.Combine(_deployedFolderPathInFileShare, applicationName + ".exe"); + } + + var parameterBuilder = new StringBuilder(); + parameterBuilder.Append($"\"{remotePSSessionHelperScript}\""); + parameterBuilder.Append($" -serverName {_deploymentParameters.ServerName}"); + parameterBuilder.Append($" -accountName {_deploymentParameters.ServerAccountName}"); + parameterBuilder.Append($" -accountPassword {_deploymentParameters.ServerAccountPassword}"); + parameterBuilder.Append($" -deployedFolderPath {_deployedFolderPathInFileShare}"); + + if (!string.IsNullOrEmpty(_deploymentParameters.DotnetRuntimePath)) + { + parameterBuilder.Append($" -dotnetRuntimePath \"{_deploymentParameters.DotnetRuntimePath}\""); + } + + parameterBuilder.Append($" -executablePath \"{executablePath}\""); + + if (!string.IsNullOrEmpty(executableParameters)) + { + parameterBuilder.Append($" -executableParameters \"{executableParameters}\""); + } + + parameterBuilder.Append($" -serverType {_deploymentParameters.ServerType}"); + parameterBuilder.Append($" -serverAction {serverAction}"); + parameterBuilder.Append($" -applicationBaseUrl {_deploymentParameters.ApplicationBaseUriHint}"); + var environmentVariables = string.Join("`,", _deploymentParameters.EnvironmentVariables.Select(envVariable => $"{envVariable.Key}={envVariable.Value}")); + parameterBuilder.Append($" -environmentVariables \"{environmentVariables}\""); + + var startInfo = new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = parameterBuilder.ToString(), + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + RedirectStandardInput = true + }; + + using (var runScriptsOnRemoteServerProcess = new Process() { StartInfo = startInfo }) + { + runScriptsOnRemoteServerProcess.EnableRaisingEvents = true; + runScriptsOnRemoteServerProcess.Exited += (sender, exitedArgs) => + { + Logger.LogInformation($"[{_deploymentParameters.ServerName} {serverAction} stdout]: script complete"); + }; + + runScriptsOnRemoteServerProcess.StartAndCaptureOutAndErrToLogger(serverAction, Logger); + + // Wait a second for the script to run or fail. The StartServer script will only terminate when the Deployer is disposed, + // so we don't want to wait for it to terminate here because it would deadlock. + await Task.Delay(TimeSpan.FromMinutes(1)); + + if (runScriptsOnRemoteServerProcess.HasExited && runScriptsOnRemoteServerProcess.ExitCode != 0) + { + throw new Exception($"Failed to execute the script on '{_deploymentParameters.ServerName}'."); + } + } + } + } + + private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs) + { + var dir = new DirectoryInfo(sourceDirName); + + if (!dir.Exists) + { + throw new DirectoryNotFoundException( + "Source directory does not exist or could not be found: " + + sourceDirName); + } + + var dirs = dir.GetDirectories(); + if (!Directory.Exists(destDirName)) + { + Directory.CreateDirectory(destDirName); + } + + var files = dir.GetFiles(); + foreach (var file in files) + { + var temppath = Path.Combine(destDirName, file.Name); + file.CopyTo(temppath, false); + } + + if (copySubDirs) + { + foreach (var subdir in dirs) + { + var temppath = Path.Combine(destDirName, subdir.Name); + DirectoryCopy(subdir.FullName, temppath, copySubDirs); + } + } + } + + private static Scripts CopyEmbeddedScriptFilesToDisk() + { + var embeddedFileNames = new[] { "RemotePSSessionHelper.ps1", "StartServer.ps1", "StopServer.ps1" }; + + // Copy the scripts from this assembly's embedded resources to the temp path on the machine where these + // tests are being run + var assembly = typeof(RemoteWindowsDeployer).GetTypeInfo().Assembly; + var embeddedFileProvider = new EmbeddedFileProvider( + assembly, + $"{assembly.GetName().Name}.Deployers.RemoteWindowsDeployer"); + + var filesOnDisk = new string[embeddedFileNames.Length]; + for (var i = 0; i < embeddedFileNames.Length; i++) + { + var embeddedFileName = embeddedFileNames[i]; + var physicalFilePath = Path.Combine(Path.GetTempPath(), embeddedFileName); + var sourceStream = embeddedFileProvider + .GetFileInfo(embeddedFileName) + .CreateReadStream(); + + using (sourceStream) + { + var destinationStream = File.Create(physicalFilePath); + using (destinationStream) + { + sourceStream.CopyTo(destinationStream); + } + } + + filesOnDisk[i] = physicalFilePath; + } + + var scripts = new Scripts(filesOnDisk[0], filesOnDisk[1], filesOnDisk[2]); + + return scripts; + } + + private class Scripts + { + public Scripts(string remotePSSessionHelper, string startServer, string stopServer) + { + RemotePSSessionHelper = remotePSSessionHelper; + StartServer = startServer; + StopServer = stopServer; + } + + public string RemotePSSessionHelper { get; } + + public string StartServer { get; } + + public string StopServer { get; } + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/RemoteWindowsDeploymentParameters.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/RemoteWindowsDeploymentParameters.cs new file mode 100644 index 0000000000000000000000000000000000000000..61d3a49325fd346cdcbbe954787838c20cdcba09 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/RemoteWindowsDeploymentParameters.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// See License.txt in the project root for license information + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class RemoteWindowsDeploymentParameters : DeploymentParameters + { + public RemoteWindowsDeploymentParameters( + string applicationPath, + string dotnetRuntimePath, + ServerType serverType, + RuntimeFlavor runtimeFlavor, + RuntimeArchitecture runtimeArchitecture, + string remoteServerFileSharePath, + string remoteServerName, + string remoteServerAccountName, + string remoteServerAccountPassword) + : base(applicationPath, serverType, runtimeFlavor, runtimeArchitecture) + { + RemoteServerFileSharePath = remoteServerFileSharePath; + ServerName = remoteServerName; + ServerAccountName = remoteServerAccountName; + ServerAccountPassword = remoteServerAccountPassword; + DotnetRuntimePath = dotnetRuntimePath; + } + + public string ServerName { get; } + + public string ServerAccountName { get; } + + public string ServerAccountPassword { get; } + + public string DotnetRuntimePath { get; } + + /// <summary> + /// The full path to the remote server's file share + /// </summary> + public string RemoteServerFileSharePath { get; } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/StartServer.ps1 b/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/StartServer.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..0bcfea647c7a95e56961e66c52c84f086c584241 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/StartServer.ps1 @@ -0,0 +1,82 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory=$true)] + [string]$deployedFolderPath, + + [Parameter(Mandatory=$false)] + [string]$dotnetRuntimePath, + + [Parameter(Mandatory=$true)] + [string]$executablePath, + + [Parameter(Mandatory=$false)] + [string]$executableParameters, + + [Parameter(Mandatory=$true)] + [string]$serverType, + + [Parameter(Mandatory=$true)] + [string]$serverName, + + [Parameter(Mandatory=$true)] + [string]$applicationBaseUrl, + + # These are of the format: key1=value1,key2=value2,key3=value3 + [Parameter(Mandatory=$false)] + [string]$environmentVariables +) + +Write-Host "Executing the start server script on machine '$serverName'" + +if ($serverType -eq "IIS") +{ + $publishedDirName=Split-Path $deployedFolderPath -Leaf + Write-Host "Creating IIS website '$publishedDirName' for path '$deployedFolderPath'" + Import-Module IISAdministration + $port=([System.Uri]$applicationBaseUrl).Port + $bindingPort="*:" + $port + ":" + New-IISSite -Name $publishedDirName -BindingInformation $bindingPort -PhysicalPath $deployedFolderPath +} +elseif (($serverType -eq "Kestrel") -or ($serverType -eq "WebListener")) +{ + if (-Not [string]::IsNullOrWhitespace($environmentVariables)) + { + Write-Host "Setting up environment variables" + foreach ($envVariablePair in $environmentVariables.Split(",")) + { + $pair=$envVariablePair.Split("="); + [Environment]::SetEnvironmentVariable($pair[0], $pair[1]) + } + } + + if ($executablePath -eq "dotnet.exe") + { + Write-Host "Setting the dotnet runtime path to the PATH environment variable" + [Environment]::SetEnvironmentVariable("PATH", "$dotnetRuntimePath") + } + + # Change the current working directory to the deployed folder to make applications work + # when they use API like Directory.GetCurrentDirectory() + cd -Path $deployedFolderPath + + $command = $executablePath + " " + $executableParameters + " --server.urls " + $applicationBaseUrl + if ($serverType -eq "Kestrel") + { + $command = $command + " --server Microsoft.AspNetCore.Server.Kestrel" + Write-Host "Executing the command '$command'" + Invoke-Expression $command + } + elseif ($serverType -eq "WebListener") + { + $command = $command + " --server Microsoft.AspNetCore.Server.HttpSys" + Write-Host "Executing the command '$command'" + Invoke-Expression $command + } +} +else +{ + throw [System.InvalidOperationException] "Server type '$serverType' is not supported." +} + +# NOTE: Make sure this is the last statement in this script as its used to get the exit code of this script +$LASTEXITCODE \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/StopServer.ps1 b/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/StopServer.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..9b66f883c229df213c753b8f2b6218b311139070 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/StopServer.ps1 @@ -0,0 +1,68 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory=$true)] + [string]$deployedFolderPath, + + [Parameter(Mandatory=$true)] + [string]$serverProcessName, + + [Parameter(Mandatory=$true)] + [string]$serverType, + + [Parameter(Mandatory=$true)] + [string]$serverName +) + +function DoesCommandExist($command) +{ + $oldPreference = $ErrorActionPreference + $ErrorActionPreference="stop" + + try + { + if (Get-Command $command) + { + return $true + } + } + catch + { + Write-Host "Command '$command' does not exist" + return $false + } + finally + { + $ErrorActionPreference=$oldPreference + } +} + +Write-Host "Executing the stop server script on machine '$serverName'" + +if ($serverType -eq "IIS") +{ + $publishedDirName=Split-Path $deployedFolderPath -Leaf + Write-Host "Stopping the IIS website '$publishedDirName'" + Import-Module IISAdministration + Stop-IISSite -Name $publishedDirName -Confirm:$false + Remove-IISSite -Name $publishedDirName -Confirm:$false + net stop w3svc + net start w3svc +} +else +{ + Write-Host "Stopping the process '$serverProcessName'" + $serverProcess=Get-Process -Name "$serverProcessName" + + if (DoesCommandExist("taskkill")) + { + # Kill the parent and child processes + & taskkill /pid $serverProcess.Id /t /f + } + else + { + Stop-Process -Id $serverProcess.Id -Force + } +} + +# NOTE: Make sure this is the last statement in this script as its used to get the exit code of this script +$LASTEXITCODE \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/SelfHostDeployer.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/SelfHostDeployer.cs new file mode 100644 index 0000000000000000000000000000000000000000..12f2b83de1ffa8ad06ada6be2e6a7c49b02770c2 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/SelfHostDeployer.cs @@ -0,0 +1,200 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + /// <summary> + /// Deployer for WebListener and Kestrel. + /// </summary> + public class SelfHostDeployer : ApplicationDeployer + { + private static readonly Regex NowListeningRegex = new Regex(@"^\s*Now listening on: (?<url>.*)$"); + private const string ApplicationStartedMessage = "Application started. Press Ctrl+C to shut down."; + + public Process HostProcess { get; private set; } + + public SelfHostDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) + : base(deploymentParameters, loggerFactory) + { + } + + public override async Task<DeploymentResult> DeployAsync() + { + using (Logger.BeginScope("SelfHost.Deploy")) + { + // Start timer + StartTimer(); + + if (DeploymentParameters.PublishApplicationBeforeDeployment) + { + DotnetPublish(); + } + + var hintUrl = TestUriHelper.BuildTestUri( + DeploymentParameters.ApplicationBaseUriHint, + DeploymentParameters.ServerType, + DeploymentParameters.StatusMessagesEnabled); + + // Launch the host process. + var (actualUrl, hostExitToken) = await StartSelfHostAsync(hintUrl); + + Logger.LogInformation("Application ready at URL: {appUrl}", actualUrl); + + return new DeploymentResult( + LoggerFactory, + DeploymentParameters, + applicationBaseUri: actualUrl.ToString(), + contentRoot: DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath, + hostShutdownToken: hostExitToken); + } + } + + protected async Task<(Uri url, CancellationToken hostExitToken)> StartSelfHostAsync(Uri hintUrl) + { + using (Logger.BeginScope("StartSelfHost")) + { + string executableName; + string executableArgs = string.Empty; + string workingDirectory = string.Empty; + if (DeploymentParameters.PublishApplicationBeforeDeployment) + { + workingDirectory = DeploymentParameters.PublishedApplicationRootPath; + var executableExtension = + DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr ? ".exe" : + DeploymentParameters.ApplicationType == ApplicationType.Portable ? ".dll" : ""; + var executable = Path.Combine(DeploymentParameters.PublishedApplicationRootPath, DeploymentParameters.ApplicationName + executableExtension); + + if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + executableName = "mono"; + executableArgs = executable; + } + else if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr && DeploymentParameters.ApplicationType == ApplicationType.Portable) + { + executableName = "dotnet"; + executableArgs = executable; + } + else + { + executableName = executable; + } + } + else + { + workingDirectory = DeploymentParameters.ApplicationPath; + var targetFramework = DeploymentParameters.TargetFramework ?? (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr ? "net461" : "netcoreapp2.0"); + + executableName = DotnetCommandName; + executableArgs = $"run --no-build -c {DeploymentParameters.Configuration} --framework {targetFramework} {DotnetArgumentSeparator}"; + } + + executableArgs += $" --server.urls {hintUrl} " + + $" --server {(DeploymentParameters.ServerType == ServerType.WebListener ? "Microsoft.AspNetCore.Server.HttpSys" : "Microsoft.AspNetCore.Server.Kestrel")}"; + + Logger.LogInformation($"Executing {executableName} {executableArgs}"); + + var startInfo = new ProcessStartInfo + { + FileName = executableName, + Arguments = executableArgs, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + // Trying a work around for https://github.com/aspnet/Hosting/issues/140. + RedirectStandardInput = true, + WorkingDirectory = workingDirectory + }; + + AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables); + + Uri actualUrl = null; + var started = new TaskCompletionSource<object>(); + + HostProcess = new Process() { StartInfo = startInfo }; + HostProcess.EnableRaisingEvents = true; + HostProcess.OutputDataReceived += (sender, dataArgs) => + { + if (string.Equals(dataArgs.Data, ApplicationStartedMessage)) + { + started.TrySetResult(null); + } + else if (!string.IsNullOrEmpty(dataArgs.Data)) + { + var m = NowListeningRegex.Match(dataArgs.Data); + if (m.Success) + { + actualUrl = new Uri(m.Groups["url"].Value); + } + } + }; + var hostExitTokenSource = new CancellationTokenSource(); + HostProcess.Exited += (sender, e) => + { + Logger.LogInformation("host process ID {pid} shut down", HostProcess.Id); + + // If TrySetResult was called above, this will just silently fail to set the new state, which is what we want + started.TrySetException(new Exception($"Command exited unexpectedly with exit code: {HostProcess.ExitCode}")); + + TriggerHostShutdown(hostExitTokenSource); + }; + + try + { + HostProcess.StartAndCaptureOutAndErrToLogger(executableName, Logger); + } + catch (Exception ex) + { + Logger.LogError("Error occurred while starting the process. Exception: {exception}", ex.ToString()); + } + + if (HostProcess.HasExited) + { + Logger.LogError("Host process {processName} {pid} exited with code {exitCode} or failed to start.", startInfo.FileName, HostProcess.Id, HostProcess.ExitCode); + throw new Exception("Failed to start host"); + } + + Logger.LogInformation("Started {fileName}. Process Id : {processId}", startInfo.FileName, HostProcess.Id); + + // Host may not write startup messages, in which case assume it started + if (DeploymentParameters.StatusMessagesEnabled) + { + // The timeout here is large, because we don't know how long the test could need + // We cover a lot of error cases above, but I want to make sure we eventually give up and don't hang the build + // just in case we missed one -anurse + await started.Task.TimeoutAfter(TimeSpan.FromMinutes(10)); + } + + return (url: actualUrl ?? hintUrl, hostExitToken: hostExitTokenSource.Token); + } + } + + public override void Dispose() + { + using (Logger.BeginScope("SelfHost.Dispose")) + { + ShutDownIfAnyHostProcess(HostProcess); + + if (DeploymentParameters.PublishApplicationBeforeDeployment) + { + CleanPublishedOutput(); + } + + InvokeUserApplicationCleanup(); + + StopTimer(); + } + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj new file mode 100644 index 0000000000000000000000000000000000000000..e693a222784e3ed8f2142a6e37a6f196f959f800 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj @@ -0,0 +1,34 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>ASP.NET Core helpers to deploy applications to IIS Express, IIS, WebListener and Kestrel for testing.</Description> + <VersionPrefix Condition="'$(ExperimentalVersionPrefix)' != ''">$(ExperimentalVersionPrefix)</VersionPrefix> + <VersionSuffix Condition="'$(ExperimentalVersionSuffix)' != ''">$(ExperimentalVersionSuffix)</VersionSuffix> + <VerifyVersion Condition="'$(ExperimentalVersionPrefix)' != ''">false</VerifyVersion> + <PackageVersion Condition="'$(ExperimentalPackageVersion)' != ''">$(ExperimentalPackageVersion)</PackageVersion> + <TargetFramework>netstandard2.0</TargetFramework> + <NoWarn>$(NoWarn);CS1591</NoWarn> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore;testing</PackageTags> + <EnableApiCheck>false</EnableApiCheck> + <UseLatestPackageReferences>true</UseLatestPackageReferences> + <IsPackable>false</IsPackable> + </PropertyGroup> + + <ItemGroup> + <EmbeddedResource Include="Deployers\RemoteWindowsDeployer\RemotePSSessionHelper.ps1;Deployers\RemoteWindowsDeployer\StartServer.ps1;Deployers\RemoteWindowsDeployer\StopServer.ps1" Exclude="bin\**;obj\**;**\*.xproj;packages\**;@(EmbeddedResource)" /> + </ItemGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Testing" /> + <Reference Include="Microsoft.Extensions.FileProviders.Embedded" /> + <Reference Include="Microsoft.Extensions.Logging" /> + <Reference Include="Microsoft.Extensions.Logging.Console" /> + <Reference Include="Microsoft.Extensions.Logging.Testing" /> + <Reference Include="Microsoft.Extensions.Process.Sources" PrivateAssets="All" /> + <Reference Include="Microsoft.NETCore.Windows.ApiSets" /> + <Reference Include="Serilog.Extensions.Logging" /> + <Reference Include="Serilog.Sinks.File" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/Server.IntegrationTesting/src/baseline.netcore.json b/src/Hosting/Server.IntegrationTesting/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..9e26dfeeb6e641a33dae4961196235bdb965b21b --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/baseline.netcore.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/xunit/SkipIfEnvironmentVariableNotEnabled.cs b/src/Hosting/Server.IntegrationTesting/src/xunit/SkipIfEnvironmentVariableNotEnabled.cs new file mode 100644 index 0000000000000000000000000000000000000000..8d54d6ac709e075531d9179b3a20c39649aa8919 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/xunit/SkipIfEnvironmentVariableNotEnabled.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + /// <summary> + /// Skip test if a given environment variable is not enabled. To enable the test, set environment variable + /// to "true" for the test process. + /// </summary> + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class SkipIfEnvironmentVariableNotEnabledAttribute : Attribute, ITestCondition + { + private readonly string _environmentVariableName; + + public SkipIfEnvironmentVariableNotEnabledAttribute(string environmentVariableName) + { + _environmentVariableName = environmentVariableName; + } + + public bool IsMet + { + get + { + return string.Compare(Environment.GetEnvironmentVariable(_environmentVariableName), "true", ignoreCase: true) == 0; + } + } + + public string SkipReason + { + get + { + return $"To run this test, set the environment variable {_environmentVariableName}=\"true\". {AdditionalInfo}"; + } + } + + public string AdditionalInfo { get; set; } + } +} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/xunit/SkipOn32BitOSAttribute.cs b/src/Hosting/Server.IntegrationTesting/src/xunit/SkipOn32BitOSAttribute.cs new file mode 100644 index 0000000000000000000000000000000000000000..554cc63edafe5e993fa785ad180dce07d309f8b4 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/xunit/SkipOn32BitOSAttribute.cs @@ -0,0 +1,33 @@ +// 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 Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + /// <summary> + /// Skips a 64 bit test if the current Windows OS is 32-bit. + /// </summary> + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class SkipOn32BitOSAttribute : Attribute, ITestCondition + { + public bool IsMet + { + get + { + // Directory found only on 64-bit OS. + return Directory.Exists(Path.Combine(Environment.GetEnvironmentVariable("SystemRoot"), "SysWOW64")); + } + } + + public string SkipReason + { + get + { + return "Skipping the x64 test since Windows is 32-bit"; + } + } + } +} \ No newline at end of file diff --git a/src/Hosting/TestHost/src/ClientHandler.cs b/src/Hosting/TestHost/src/ClientHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..2109809d1a8e79d6d2eeafb9f824fc7515dcd07c --- /dev/null +++ b/src/Hosting/TestHost/src/ClientHandler.cs @@ -0,0 +1,132 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Context = Microsoft.AspNetCore.Hosting.Internal.HostingApplication.Context; + +namespace Microsoft.AspNetCore.TestHost +{ + /// <summary> + /// This adapts HttpRequestMessages to ASP.NET Core requests, dispatches them through the pipeline, and returns the + /// associated HttpResponseMessage. + /// </summary> + public class ClientHandler : HttpMessageHandler + { + private readonly IHttpApplication<Context> _application; + private readonly PathString _pathBase; + + /// <summary> + /// Create a new handler. + /// </summary> + /// <param name="pathBase">The base path.</param> + /// <param name="application">The <see cref="IHttpApplication{TContext}"/>.</param> + public ClientHandler(PathString pathBase, IHttpApplication<Context> application) + { + _application = application ?? throw new ArgumentNullException(nameof(application)); + + // PathString.StartsWithSegments that we use below requires the base path to not end in a slash. + if (pathBase.HasValue && pathBase.Value.EndsWith("/")) + { + pathBase = new PathString(pathBase.Value.Substring(0, pathBase.Value.Length - 1)); + } + _pathBase = pathBase; + } + + /// <summary> + /// This adapts HttpRequestMessages to ASP.NET Core requests, dispatches them through the pipeline, and returns the + /// associated HttpResponseMessage. + /// </summary> + /// <param name="request"></param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + protected override async Task<HttpResponseMessage> SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + var contextBuilder = new HttpContextBuilder(_application); + + Stream responseBody = null; + var requestContent = request.Content ?? new StreamContent(Stream.Null); + var body = await requestContent.ReadAsStreamAsync(); + contextBuilder.Configure(context => + { + var req = context.Request; + + req.Protocol = "HTTP/" + request.Version.ToString(fieldCount: 2); + req.Method = request.Method.ToString(); + + req.Scheme = request.RequestUri.Scheme; + req.Host = HostString.FromUriComponent(request.RequestUri); + if (request.RequestUri.IsDefaultPort) + { + req.Host = new HostString(req.Host.Host); + } + + req.Path = PathString.FromUriComponent(request.RequestUri); + req.PathBase = PathString.Empty; + if (req.Path.StartsWithSegments(_pathBase, out var remainder)) + { + req.Path = remainder; + req.PathBase = _pathBase; + } + req.QueryString = QueryString.FromUriComponent(request.RequestUri); + + foreach (var header in request.Headers) + { + req.Headers.Append(header.Key, header.Value.ToArray()); + } + if (requestContent != null) + { + foreach (var header in requestContent.Headers) + { + req.Headers.Append(header.Key, header.Value.ToArray()); + } + } + + if (body.CanSeek) + { + // This body may have been consumed before, rewind it. + body.Seek(0, SeekOrigin.Begin); + } + req.Body = body; + + responseBody = context.Response.Body; + }); + + var httpContext = await contextBuilder.SendAsync(cancellationToken); + + var response = new HttpResponseMessage(); + response.StatusCode = (HttpStatusCode)httpContext.Response.StatusCode; + response.ReasonPhrase = httpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase; + response.RequestMessage = request; + + response.Content = new StreamContent(responseBody); + + foreach (var header in httpContext.Response.Headers) + { + if (!response.Headers.TryAddWithoutValidation(header.Key, (IEnumerable<string>)header.Value)) + { + bool success = response.Content.Headers.TryAddWithoutValidation(header.Key, (IEnumerable<string>)header.Value); + Contract.Assert(success, "Bad header"); + } + } + return response; + } + } +} diff --git a/src/Hosting/TestHost/src/HttpContextBuilder.cs b/src/Hosting/TestHost/src/HttpContextBuilder.cs new file mode 100644 index 0000000000000000000000000000000000000000..6886b1aac45a8dc515fad74ee31aa32059bc045b --- /dev/null +++ b/src/Hosting/TestHost/src/HttpContextBuilder.cs @@ -0,0 +1,130 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using static Microsoft.AspNetCore.Hosting.Internal.HostingApplication; + +namespace Microsoft.AspNetCore.TestHost +{ + internal class HttpContextBuilder + { + private readonly IHttpApplication<Context> _application; + private readonly HttpContext _httpContext; + + private TaskCompletionSource<HttpContext> _responseTcs = new TaskCompletionSource<HttpContext>(TaskCreationOptions.RunContinuationsAsynchronously); + private ResponseStream _responseStream; + private ResponseFeature _responseFeature = new ResponseFeature(); + private CancellationTokenSource _requestAbortedSource = new CancellationTokenSource(); + private bool _pipelineFinished; + private Context _testContext; + + internal HttpContextBuilder(IHttpApplication<Context> application) + { + _application = application ?? throw new ArgumentNullException(nameof(application)); + _httpContext = new DefaultHttpContext(); + + var request = _httpContext.Request; + request.Protocol = "HTTP/1.1"; + request.Method = HttpMethods.Get; + + _httpContext.Features.Set<IHttpResponseFeature>(_responseFeature); + var requestLifetimeFeature = new HttpRequestLifetimeFeature(); + requestLifetimeFeature.RequestAborted = _requestAbortedSource.Token; + _httpContext.Features.Set<IHttpRequestLifetimeFeature>(requestLifetimeFeature); + + _responseStream = new ResponseStream(ReturnResponseMessageAsync, AbortRequest); + _responseFeature.Body = _responseStream; + } + + internal void Configure(Action<HttpContext> configureContext) + { + if (configureContext == null) + { + throw new ArgumentNullException(nameof(configureContext)); + } + + configureContext(_httpContext); + } + + /// <summary> + /// Start processing the request. + /// </summary> + /// <returns></returns> + internal Task<HttpContext> SendAsync(CancellationToken cancellationToken) + { + var registration = cancellationToken.Register(AbortRequest); + + _testContext = _application.CreateContext(_httpContext.Features); + + // Async offload, don't let the test code block the caller. + _ = Task.Factory.StartNew(async () => + { + try + { + await _application.ProcessRequestAsync(_testContext); + await CompleteResponseAsync(); + _application.DisposeContext(_testContext, exception: null); + } + catch (Exception ex) + { + Abort(ex); + _application.DisposeContext(_testContext, ex); + } + finally + { + registration.Dispose(); + } + }); + + return _responseTcs.Task; + } + + internal void AbortRequest() + { + if (!_pipelineFinished) + { + _requestAbortedSource.Cancel(); + } + _responseStream.CompleteWrites(); + } + + internal async Task CompleteResponseAsync() + { + _pipelineFinished = true; + await ReturnResponseMessageAsync(); + _responseStream.CompleteWrites(); + await _responseFeature.FireOnResponseCompletedAsync(); + } + + internal async Task ReturnResponseMessageAsync() + { + // Check if the response has already started because the TrySetResult below could happen a bit late + // (as it happens on a different thread) by which point the CompleteResponseAsync could run and calls this + // method again. + if (!_responseFeature.HasStarted) + { + // Sets HasStarted + await _responseFeature.FireOnSendingHeadersAsync(); + // Copy the feature collection so we're not multi-threading on the same collection. + var newFeatures = new FeatureCollection(); + foreach (var pair in _httpContext.Features) + { + newFeatures[pair.Key] = pair.Value; + } + _responseTcs.TrySetResult(new DefaultHttpContext(newFeatures)); + } + } + + internal void Abort(Exception exception) + { + _pipelineFinished = true; + _responseStream.Abort(exception); + _responseTcs.TrySetException(exception); + } + } +} \ No newline at end of file diff --git a/src/Hosting/TestHost/src/Microsoft.AspNetCore.TestHost.csproj b/src/Hosting/TestHost/src/Microsoft.AspNetCore.TestHost.csproj new file mode 100644 index 0000000000000000000000000000000000000000..acb65b20062ec0004d532c12182eced6e86bdddd --- /dev/null +++ b/src/Hosting/TestHost/src/Microsoft.AspNetCore.TestHost.csproj @@ -0,0 +1,20 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>ASP.NET Core web server for writing and running tests.</Description> + <TargetFramework>netstandard2.0</TargetFramework> + <NoWarn>$(NoWarn);CS1591</NoWarn> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore;hosting;testing</PackageTags> + </PropertyGroup> + + <ItemGroup> + <Compile Include="$(SharedSourceRoot)Hosting.WebHostBuilderFactory\**\*.cs" /> + </ItemGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Hosting" /> + <Reference Include="System.IO.Pipelines" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/TestHost/src/Properties/AssemblyInfo.cs b/src/Hosting/TestHost/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..5e17a096e7803190a7a65dafab46bcbe3d7778d6 --- /dev/null +++ b/src/Hosting/TestHost/src/Properties/AssemblyInfo.cs @@ -0,0 +1,10 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: ComVisible(false)] +[assembly: Guid("12A3EDBB-65B6-4D47-98FC-2B80CEC71E51")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.TestHost.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] + diff --git a/src/Hosting/TestHost/src/RequestBuilder.cs b/src/Hosting/TestHost/src/RequestBuilder.cs new file mode 100644 index 0000000000000000000000000000000000000000..c770ec75babf4cba60a27d9ddc14eaa76f65f65a --- /dev/null +++ b/src/Hosting/TestHost/src/RequestBuilder.cs @@ -0,0 +1,105 @@ +// 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.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.TestHost +{ + /// <summary> + /// Used to construct a HttpRequestMessage object. + /// </summary> + public class RequestBuilder + { + private readonly TestServer _server; + private readonly HttpRequestMessage _req; + + /// <summary> + /// Construct a new HttpRequestMessage with the given path. + /// </summary> + /// <param name="server"></param> + /// <param name="path"></param> + public RequestBuilder(TestServer server, string path) + { + if (server == null) + { + throw new ArgumentNullException(nameof(server)); + } + + _server = server; + _req = new HttpRequestMessage(HttpMethod.Get, path); + } + + /// <summary> + /// Configure any HttpRequestMessage properties. + /// </summary> + /// <param name="configure"></param> + /// <returns></returns> + public RequestBuilder And(Action<HttpRequestMessage> configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + configure(_req); + return this; + } + + /// <summary> + /// Add the given header and value to the request or request content. + /// </summary> + /// <param name="name"></param> + /// <param name="value"></param> + /// <returns></returns> + public RequestBuilder AddHeader(string name, string value) + { + if (!_req.Headers.TryAddWithoutValidation(name, value)) + { + if (_req.Content == null) + { + _req.Content = new StreamContent(Stream.Null); + } + if (!_req.Content.Headers.TryAddWithoutValidation(name, value)) + { + // TODO: throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.InvalidHeaderName, name), "name"); + throw new ArgumentException("Invalid header name: " + name, "name"); + } + } + return this; + } + + /// <summary> + /// Set the request method and start processing the request. + /// </summary> + /// <param name="method"></param> + /// <returns></returns> + public Task<HttpResponseMessage> SendAsync(string method) + { + _req.Method = new HttpMethod(method); + return _server.CreateClient().SendAsync(_req); + } + + /// <summary> + /// Set the request method to GET and start processing the request. + /// </summary> + /// <returns></returns> + public Task<HttpResponseMessage> GetAsync() + { + _req.Method = HttpMethod.Get; + return _server.CreateClient().SendAsync(_req); + } + + /// <summary> + /// Set the request method to POST and start processing the request. + /// </summary> + /// <returns></returns> + public Task<HttpResponseMessage> PostAsync() + { + _req.Method = HttpMethod.Post; + return _server.CreateClient().SendAsync(_req); + } + } +} diff --git a/src/Hosting/TestHost/src/RequestFeature.cs b/src/Hosting/TestHost/src/RequestFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..d634f2dbe2a7fce4106e4ae157c115e3abd0cc12 --- /dev/null +++ b/src/Hosting/TestHost/src/RequestFeature.cs @@ -0,0 +1,42 @@ +// 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.IO; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.TestHost +{ + internal class RequestFeature : IHttpRequestFeature + { + public RequestFeature() + { + Body = Stream.Null; + Headers = new HeaderDictionary(); + Method = "GET"; + Path = ""; + PathBase = ""; + Protocol = "HTTP/1.1"; + QueryString = ""; + Scheme = "http"; + } + + public Stream Body { get; set; } + + public IHeaderDictionary Headers { get; set; } + + public string Method { get; set; } + + public string Path { get; set; } + + public string PathBase { get; set; } + + public string Protocol { get; set; } + + public string QueryString { get; set; } + + public string Scheme { get; set; } + + public string RawTarget { get; set; } + } +} diff --git a/src/Hosting/TestHost/src/ResponseFeature.cs b/src/Hosting/TestHost/src/ResponseFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..c6c7b47e18ddfe25022cf26423ec24f2db75d508 --- /dev/null +++ b/src/Hosting/TestHost/src/ResponseFeature.cs @@ -0,0 +1,111 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.TestHost +{ + internal class ResponseFeature : IHttpResponseFeature + { + private Func<Task> _responseStartingAsync = () => Task.FromResult(true); + private Func<Task> _responseCompletedAsync = () => Task.FromResult(true); + private HeaderDictionary _headers = new HeaderDictionary(); + private int _statusCode; + private string _reasonPhrase; + + public ResponseFeature() + { + Headers = _headers; + Body = new MemoryStream(); + + // 200 is the default status code all the way down to the host, so we set it + // here to be consistent with the rest of the hosts when writing tests. + StatusCode = 200; + } + + public int StatusCode + { + get => _statusCode; + set + { + if (HasStarted) + { + throw new InvalidOperationException("The status code cannot be set, the response has already started."); + } + if (value < 100) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "The status code cannot be set to a value less than 100"); + } + + _statusCode = value; + } + } + + public string ReasonPhrase + { + get => _reasonPhrase; + set + { + if (HasStarted) + { + throw new InvalidOperationException("The reason phrase cannot be set, the response has already started."); + } + + _reasonPhrase = value; + } + } + + public IHeaderDictionary Headers { get; set; } + + public Stream Body { get; set; } + + public bool HasStarted { get; set; } + + public void OnStarting(Func<object, Task> callback, object state) + { + if (HasStarted) + { + throw new InvalidOperationException(); + } + + var prior = _responseStartingAsync; + _responseStartingAsync = async () => + { + await callback(state); + await prior(); + }; + } + + public void OnCompleted(Func<object, Task> callback, object state) + { + var prior = _responseCompletedAsync; + _responseCompletedAsync = async () => + { + try + { + await callback(state); + } + finally + { + await prior(); + } + }; + } + + public async Task FireOnSendingHeadersAsync() + { + await _responseStartingAsync(); + HasStarted = true; + _headers.IsReadOnly = true; + } + + public Task FireOnResponseCompletedAsync() + { + return _responseCompletedAsync(); + } + } +} diff --git a/src/Hosting/TestHost/src/ResponseStream.cs b/src/Hosting/TestHost/src/ResponseStream.cs new file mode 100644 index 0000000000000000000000000000000000000000..0cd3459a80e841436208c6f6d0350f578e5aa1bb --- /dev/null +++ b/src/Hosting/TestHost/src/ResponseStream.cs @@ -0,0 +1,256 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.IO; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.TestHost +{ + // This steam accepts writes from the server/app, buffers them internally, and returns the data via Reads + // when requested by the client. + internal class ResponseStream : Stream + { + private bool _complete; + private bool _aborted; + private Exception _abortException; + private SemaphoreSlim _writeLock; + + private Func<Task> _onFirstWriteAsync; + private bool _firstWrite; + private Action _abortRequest; + + private Pipe _pipe = new Pipe(); + + internal ResponseStream(Func<Task> onFirstWriteAsync, Action abortRequest) + { + _onFirstWriteAsync = onFirstWriteAsync ?? throw new ArgumentNullException(nameof(onFirstWriteAsync)); + _abortRequest = abortRequest ?? throw new ArgumentNullException(nameof(abortRequest)); + _firstWrite = true; + _writeLock = new SemaphoreSlim(1, 1); + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return true; } + } + + #region NotSupported + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + #endregion NotSupported + + public override void Flush() + { + FlushAsync().GetAwaiter().GetResult(); + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + CheckNotComplete(); + + await _writeLock.WaitAsync(cancellationToken); + try + { + await FirstWriteAsync(); + await _pipe.Writer.FlushAsync(cancellationToken); + } + finally + { + _writeLock.Release(); + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + } + + public async override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + VerifyBuffer(buffer, offset, count, allowEmpty: false); + CheckAborted(); + var registration = cancellationToken.Register(Cancel); + try + { + // TODO: Usability issue. dotnet/corefx#27732 Flush or zero byte write causes ReadAsync to complete without data so I have to call ReadAsync in a loop. + while (true) + { + var result = await _pipe.Reader.ReadAsync(cancellationToken); + + var readableBuffer = result.Buffer; + if (!readableBuffer.IsEmpty) + { + var actual = Math.Min(readableBuffer.Length, count); + readableBuffer = readableBuffer.Slice(0, actual); + readableBuffer.CopyTo(new Span<byte>(buffer, offset, count)); + _pipe.Reader.AdvanceTo(readableBuffer.End, readableBuffer.End); + return (int)actual; + } + + if (result.IsCompleted) + { + _pipe.Reader.AdvanceTo(readableBuffer.End, readableBuffer.End); // TODO: Remove after https://github.com/dotnet/corefx/pull/27596 + _pipe.Reader.Complete(); + return 0; + } + + cancellationToken.ThrowIfCancellationRequested(); + Debug.Assert(!result.IsCanceled); // It should only be canceled by cancellationToken. + + // Try again. TODO: dotnet/corefx#27732 I shouldn't need to do this, there wasn't any data. + _pipe.Reader.AdvanceTo(readableBuffer.End, readableBuffer.End); + } + } + finally + { + registration.Dispose(); + } + } + + // Called under write-lock. + private Task FirstWriteAsync() + { + if (_firstWrite) + { + _firstWrite = false; + return _onFirstWriteAsync(); + } + return Task.FromResult(true); + } + + // Write with count 0 will still trigger OnFirstWrite + public override void Write(byte[] buffer, int offset, int count) + { + // The Pipe Write method requires calling FlushAsync to notify the reader. Call WriteAsync instead. + WriteAsync(buffer, offset, count).GetAwaiter().GetResult(); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + VerifyBuffer(buffer, offset, count, allowEmpty: true); + CheckNotComplete(); + + await _writeLock.WaitAsync(cancellationToken); + try + { + await FirstWriteAsync(); + await _pipe.Writer.WriteAsync(new ReadOnlyMemory<byte>(buffer, offset, count), cancellationToken); + } + finally + { + _writeLock.Release(); + } + } + + private static void VerifyBuffer(byte[] buffer, int offset, int count, bool allowEmpty) + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentOutOfRangeException("offset", offset, string.Empty); + } + if (count < 0 || count > buffer.Length - offset + || (!allowEmpty && count == 0)) + { + throw new ArgumentOutOfRangeException("count", count, string.Empty); + } + } + + internal void Cancel() + { + _aborted = true; + _abortException = new OperationCanceledException(); + _complete = true; + _pipe.Writer.Complete(_abortException); + } + + internal void Abort(Exception innerException) + { + Contract.Requires(innerException != null); + _aborted = true; + _abortException = innerException; + _complete = true; + _pipe.Writer.Complete(new IOException(string.Empty, innerException)); + } + + internal void CompleteWrites() + { + // If HttpClient.Dispose gets called while HttpClient.SetTask...() is called + // there is a chance that this method will be called twice and hang on the lock + // to prevent this we can check if there is already a thread inside the lock + if (_complete) + { + return; + } + + // Throw for further writes, but not reads. Allow reads to drain the buffered data and then return 0 for further reads. + _complete = true; + _pipe.Writer.Complete(); + } + + private void CheckAborted() + { + if (_aborted) + { + throw new IOException(string.Empty, _abortException); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _abortRequest(); + } + base.Dispose(disposing); + } + + private void CheckNotComplete() + { + if (_complete) + { + throw new IOException("The request was aborted or the pipeline has finished"); + } + } + } +} diff --git a/src/Hosting/TestHost/src/TestServer.cs b/src/Hosting/TestHost/src/TestServer.cs new file mode 100644 index 0000000000000000000000000000000000000000..398a575d9df06e38fe0aa6129c56b60c8a47d234 --- /dev/null +++ b/src/Hosting/TestHost/src/TestServer.cs @@ -0,0 +1,172 @@ +// 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.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Context = Microsoft.AspNetCore.Hosting.Internal.HostingApplication.Context; + +namespace Microsoft.AspNetCore.TestHost +{ + public class TestServer : IServer + { + private const string ServerName = nameof(TestServer); + private IWebHost _hostInstance; + private bool _disposed = false; + private IHttpApplication<Context> _application; + + public TestServer(IWebHostBuilder builder) + : this(builder, new FeatureCollection()) + { + } + + public TestServer(IWebHostBuilder builder, IFeatureCollection featureCollection) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + if (featureCollection == null) + { + throw new ArgumentNullException(nameof(featureCollection)); + } + + Features = featureCollection; + + var host = builder.UseServer(this).Build(); + host.StartAsync().GetAwaiter().GetResult(); + _hostInstance = host; + } + + public Uri BaseAddress { get; set; } = new Uri("http://localhost/"); + + public IWebHost Host + { + get + { + return _hostInstance; + } + } + + public IFeatureCollection Features { get; } + + public HttpMessageHandler CreateHandler() + { + var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); + return new ClientHandler(pathBase, _application); + } + + public HttpClient CreateClient() + { + return new HttpClient(CreateHandler()) { BaseAddress = BaseAddress }; + } + + public WebSocketClient CreateWebSocketClient() + { + var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); + return new WebSocketClient(pathBase, _application); + } + + /// <summary> + /// Begins constructing a request message for submission. + /// </summary> + /// <param name="path"></param> + /// <returns><see cref="RequestBuilder"/> to use in constructing additional request details.</returns> + public RequestBuilder CreateRequest(string path) + { + return new RequestBuilder(this, path); + } + + /// <summary> + /// Creates, configures, sends, and returns a <see cref="HttpContext"/>. This completes as soon as the response is started. + /// </summary> + /// <returns></returns> + public async Task<HttpContext> SendAsync(Action<HttpContext> configureContext, CancellationToken cancellationToken = default) + { + if (configureContext == null) + { + throw new ArgumentNullException(nameof(configureContext)); + } + + var builder = new HttpContextBuilder(_application); + builder.Configure(context => + { + var request = context.Request; + request.Scheme = BaseAddress.Scheme; + request.Host = HostString.FromUriComponent(BaseAddress); + if (BaseAddress.IsDefaultPort) + { + request.Host = new HostString(request.Host.Host); + } + var pathBase = PathString.FromUriComponent(BaseAddress); + if (pathBase.HasValue && pathBase.Value.EndsWith("/")) + { + pathBase = new PathString(pathBase.Value.Substring(0, pathBase.Value.Length - 1)); + } + request.PathBase = pathBase; + }); + builder.Configure(configureContext); + return await builder.SendAsync(cancellationToken).ConfigureAwait(false); + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + _hostInstance.Dispose(); + } + } + + Task IServer.StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) + { + _application = new ApplicationWrapper<Context>((IHttpApplication<Context>)application, () => + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + }); + + return Task.CompletedTask; + } + + Task IServer.StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + private class ApplicationWrapper<TContext> : IHttpApplication<TContext> + { + private readonly IHttpApplication<TContext> _application; + private readonly Action _preProcessRequestAsync; + + public ApplicationWrapper(IHttpApplication<TContext> application, Action preProcessRequestAsync) + { + _application = application; + _preProcessRequestAsync = preProcessRequestAsync; + } + + public TContext CreateContext(IFeatureCollection contextFeatures) + { + return _application.CreateContext(contextFeatures); + } + + public void DisposeContext(TContext context, Exception exception) + { + _application.DisposeContext(context, exception); + } + + public Task ProcessRequestAsync(TContext context) + { + _preProcessRequestAsync(); + return _application.ProcessRequestAsync(context); + } + } + } +} diff --git a/src/Hosting/TestHost/src/TestWebSocket.cs b/src/Hosting/TestHost/src/TestWebSocket.cs new file mode 100644 index 0000000000000000000000000000000000000000..d1a77e5af6634d67628b8dbf716de14f4d3537d8 --- /dev/null +++ b/src/Hosting/TestHost/src/TestWebSocket.cs @@ -0,0 +1,354 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.TestHost +{ + internal class TestWebSocket : WebSocket + { + private ReceiverSenderBuffer _receiveBuffer; + private ReceiverSenderBuffer _sendBuffer; + private readonly string _subProtocol; + private WebSocketState _state; + private WebSocketCloseStatus? _closeStatus; + private string _closeStatusDescription; + private Message _receiveMessage; + + public static Tuple<TestWebSocket, TestWebSocket> CreatePair(string subProtocol) + { + var buffers = new[] { new ReceiverSenderBuffer(), new ReceiverSenderBuffer() }; + return Tuple.Create( + new TestWebSocket(subProtocol, buffers[0], buffers[1]), + new TestWebSocket(subProtocol, buffers[1], buffers[0])); + } + + public override WebSocketCloseStatus? CloseStatus + { + get { return _closeStatus; } + } + + public override string CloseStatusDescription + { + get { return _closeStatusDescription; } + } + + public override WebSocketState State + { + get { return _state; } + } + + public override string SubProtocol + { + get { return _subProtocol; } + } + + public async override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + + if (State == WebSocketState.Open || State == WebSocketState.CloseReceived) + { + // Send a close message. + await CloseOutputAsync(closeStatus, statusDescription, cancellationToken); + } + + if (State == WebSocketState.CloseSent) + { + // Do a receiving drain + var data = new byte[1024]; + WebSocketReceiveResult result; + do + { + result = await ReceiveAsync(new ArraySegment<byte>(data), cancellationToken); + } + while (result.MessageType != WebSocketMessageType.Close); + } + } + + public async override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + ThrowIfOutputClosed(); + + var message = new Message(closeStatus, statusDescription); + await _sendBuffer.SendAsync(message, cancellationToken); + + if (State == WebSocketState.Open) + { + _state = WebSocketState.CloseSent; + } + else if (State == WebSocketState.CloseReceived) + { + _state = WebSocketState.Closed; + Close(); + } + } + + public override void Abort() + { + if (_state >= WebSocketState.Closed) // or Aborted + { + return; + } + + _state = WebSocketState.Aborted; + Close(); + } + + public override void Dispose() + { + if (_state >= WebSocketState.Closed) // or Aborted + { + return; + } + + _state = WebSocketState.Closed; + Close(); + } + + public override async Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + ThrowIfInputClosed(); + ValidateSegment(buffer); + // TODO: InvalidOperationException if any receives are currently in progress. + + Message receiveMessage = _receiveMessage; + _receiveMessage = null; + if (receiveMessage == null) + { + receiveMessage = await _receiveBuffer.ReceiveAsync(cancellationToken); + } + if (receiveMessage.MessageType == WebSocketMessageType.Close) + { + _closeStatus = receiveMessage.CloseStatus; + _closeStatusDescription = receiveMessage.CloseStatusDescription ?? string.Empty; + var result = new WebSocketReceiveResult(0, WebSocketMessageType.Close, true, _closeStatus, _closeStatusDescription); + if (_state == WebSocketState.Open) + { + _state = WebSocketState.CloseReceived; + } + else if (_state == WebSocketState.CloseSent) + { + _state = WebSocketState.Closed; + Close(); + } + return result; + } + else + { + int count = Math.Min(buffer.Count, receiveMessage.Buffer.Count); + bool endOfMessage = count == receiveMessage.Buffer.Count; + Array.Copy(receiveMessage.Buffer.Array, receiveMessage.Buffer.Offset, buffer.Array, buffer.Offset, count); + if (!endOfMessage) + { + receiveMessage.Buffer = new ArraySegment<byte>(receiveMessage.Buffer.Array, receiveMessage.Buffer.Offset + count, receiveMessage.Buffer.Count - count); + _receiveMessage = receiveMessage; + } + endOfMessage = endOfMessage && receiveMessage.EndOfMessage; + return new WebSocketReceiveResult(count, receiveMessage.MessageType, endOfMessage); + } + } + + public override Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + { + ValidateSegment(buffer); + if (messageType != WebSocketMessageType.Binary && messageType != WebSocketMessageType.Text) + { + // Block control frames + throw new ArgumentOutOfRangeException(nameof(messageType), messageType, string.Empty); + } + + var message = new Message(buffer, messageType, endOfMessage, cancellationToken); + return _sendBuffer.SendAsync(message, cancellationToken); + } + + private void Close() + { + _receiveBuffer.SetReceiverClosed(); + _sendBuffer.SetSenderClosed(); + } + + private void ThrowIfDisposed() + { + if (_state >= WebSocketState.Closed) // or Aborted + { + throw new ObjectDisposedException(typeof(TestWebSocket).FullName); + } + } + + private void ThrowIfOutputClosed() + { + if (State == WebSocketState.CloseSent) + { + throw new InvalidOperationException("Close already sent."); + } + } + + private void ThrowIfInputClosed() + { + if (State == WebSocketState.CloseReceived) + { + throw new InvalidOperationException("Close already received."); + } + } + + private void ValidateSegment(ArraySegment<byte> buffer) + { + if (buffer.Array == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (buffer.Offset < 0 || buffer.Offset > buffer.Array.Length) + { + throw new ArgumentOutOfRangeException(nameof(buffer.Offset), buffer.Offset, string.Empty); + } + if (buffer.Count < 0 || buffer.Count > buffer.Array.Length - buffer.Offset) + { + throw new ArgumentOutOfRangeException(nameof(buffer.Count), buffer.Count, string.Empty); + } + } + + private TestWebSocket(string subProtocol, ReceiverSenderBuffer readBuffer, ReceiverSenderBuffer writeBuffer) + { + _state = WebSocketState.Open; + _subProtocol = subProtocol; + _receiveBuffer = readBuffer; + _sendBuffer = writeBuffer; + } + + private class Message + { + public Message(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken token) + { + Buffer = buffer; + CloseStatus = null; + CloseStatusDescription = null; + EndOfMessage = endOfMessage; + MessageType = messageType; + } + + public Message(WebSocketCloseStatus? closeStatus, string closeStatusDescription) + { + Buffer = new ArraySegment<byte>(new byte[0]); + CloseStatus = closeStatus; + CloseStatusDescription = closeStatusDescription; + MessageType = WebSocketMessageType.Close; + EndOfMessage = true; + } + + public WebSocketCloseStatus? CloseStatus { get; set; } + public string CloseStatusDescription { get; set; } + public ArraySegment<byte> Buffer { get; set; } + public bool EndOfMessage { get; set; } + public WebSocketMessageType MessageType { get; set; } + } + + private class ReceiverSenderBuffer + { + private bool _receiverClosed; + private bool _senderClosed; + private bool _disposed; + private SemaphoreSlim _sem; + private Queue<Message> _messageQueue; + + public ReceiverSenderBuffer() + { + _sem = new SemaphoreSlim(0); + _messageQueue = new Queue<Message>(); + } + + public async virtual Task<Message> ReceiveAsync(CancellationToken cancellationToken) + { + if (_disposed) + { + ThrowNoReceive(); + } + await _sem.WaitAsync(cancellationToken); + lock (_messageQueue) + { + if (_messageQueue.Count == 0) + { + _disposed = true; + _sem.Dispose(); + ThrowNoReceive(); + } + return _messageQueue.Dequeue(); + } + } + + public virtual Task SendAsync(Message message, CancellationToken cancellationToken) + { + lock (_messageQueue) + { + if (_senderClosed) + { + throw new ObjectDisposedException(typeof(TestWebSocket).FullName); + } + if (_receiverClosed) + { + throw new IOException("The remote end closed the connection.", new ObjectDisposedException(typeof(TestWebSocket).FullName)); + } + + // we return immediately so we need to copy the buffer since the sender can re-use it + var array = new byte[message.Buffer.Count]; + Array.Copy(message.Buffer.Array, message.Buffer.Offset, array, 0, message.Buffer.Count); + message.Buffer = new ArraySegment<byte>(array); + + _messageQueue.Enqueue(message); + _sem.Release(); + + return Task.FromResult(true); + } + } + + public void SetReceiverClosed() + { + lock (_messageQueue) + { + if (!_receiverClosed) + { + _receiverClosed = true; + if (!_disposed) + { + _sem.Release(); + } + } + } + } + + public void SetSenderClosed() + { + lock (_messageQueue) + { + if (!_senderClosed) + { + _senderClosed = true; + if (!_disposed) + { + _sem.Release(); + } + } + } + } + + private void ThrowNoReceive() + { + if (_receiverClosed) + { + throw new ObjectDisposedException(typeof(TestWebSocket).FullName); + } + else // _senderClosed + { + throw new IOException("The remote end closed the connection.", new ObjectDisposedException(typeof(TestWebSocket).FullName)); + } + } + } + } +} diff --git a/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs b/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..cccf19cdd5ec7a721c85152a26a9b2d631b50a8f --- /dev/null +++ b/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs @@ -0,0 +1,138 @@ +// 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.Linq; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.TestHost +{ + public static class WebHostBuilderExtensions + { + public static IWebHostBuilder ConfigureTestServices(this IWebHostBuilder webHostBuilder, Action<IServiceCollection> servicesConfiguration) + { + if (webHostBuilder == null) + { + throw new ArgumentNullException(nameof(webHostBuilder)); + } + + if (servicesConfiguration == null) + { + throw new ArgumentNullException(nameof(servicesConfiguration)); + } + + webHostBuilder.ConfigureServices( + s => s.AddSingleton<IStartupConfigureServicesFilter>( + new ConfigureTestServicesStartupConfigureServicesFilter(servicesConfiguration))); + + return webHostBuilder; + } + + public static IWebHostBuilder ConfigureTestContainer<TContainer>(this IWebHostBuilder webHostBuilder, Action<TContainer> servicesConfiguration) + { + if (webHostBuilder == null) + { + throw new ArgumentNullException(nameof(webHostBuilder)); + } + + if (servicesConfiguration == null) + { + throw new ArgumentNullException(nameof(servicesConfiguration)); + } + + webHostBuilder.ConfigureServices( + s => s.AddSingleton<IStartupConfigureContainerFilter<TContainer>>( + new ConfigureTestServicesStartupConfigureContainerFilter<TContainer>(servicesConfiguration))); + + return webHostBuilder; + } + + public static IWebHostBuilder UseSolutionRelativeContentRoot( + this IWebHostBuilder builder, + string solutionRelativePath, + string solutionName = "*.sln") + { + return builder.UseSolutionRelativeContentRoot(solutionRelativePath, AppContext.BaseDirectory, solutionName); + } + + public static IWebHostBuilder UseSolutionRelativeContentRoot( + this IWebHostBuilder builder, + string solutionRelativePath, + string applicationBasePath, + string solutionName = "*.sln") + { + if (solutionRelativePath == null) + { + throw new ArgumentNullException(nameof(solutionRelativePath)); + } + + if (applicationBasePath == null) + { + throw new ArgumentNullException(nameof(applicationBasePath)); + } + + var directoryInfo = new DirectoryInfo(applicationBasePath); + do + { + var solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, solutionName).FirstOrDefault(); + if (solutionPath != null) + { + builder.UseContentRoot(Path.GetFullPath(Path.Combine(directoryInfo.FullName, solutionRelativePath))); + return builder; + } + + directoryInfo = directoryInfo.Parent; + } + while (directoryInfo.Parent != null); + + throw new InvalidOperationException($"Solution root could not be located using application root {applicationBasePath}."); + } + + private class ConfigureTestServicesStartupConfigureServicesFilter : IStartupConfigureServicesFilter + { + private readonly Action<IServiceCollection> _servicesConfiguration; + + public ConfigureTestServicesStartupConfigureServicesFilter(Action<IServiceCollection> servicesConfiguration) + { + if (servicesConfiguration == null) + { + throw new ArgumentNullException(nameof(servicesConfiguration)); + } + + _servicesConfiguration = servicesConfiguration; + } + + public Action<IServiceCollection> ConfigureServices(Action<IServiceCollection> next) => + serviceCollection => + { + next(serviceCollection); + _servicesConfiguration(serviceCollection); + }; + } + + private class ConfigureTestServicesStartupConfigureContainerFilter<TContainer> : IStartupConfigureContainerFilter<TContainer> + { + private readonly Action<TContainer> _servicesConfiguration; + + public ConfigureTestServicesStartupConfigureContainerFilter(Action<TContainer> containerConfiguration) + { + if (containerConfiguration == null) + { + throw new ArgumentNullException(nameof(containerConfiguration)); + } + + _servicesConfiguration = containerConfiguration; + } + + public Action<TContainer> ConfigureContainer(Action<TContainer> next) => + containerBuilder => + { + next(containerBuilder); + _servicesConfiguration(containerBuilder); + }; + } + } +} diff --git a/src/Hosting/TestHost/src/WebHostBuilderFactory.cs b/src/Hosting/TestHost/src/WebHostBuilderFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..3a0b0a552e9b60da883a4948bb3a0ee9d9b3e2dd --- /dev/null +++ b/src/Hosting/TestHost/src/WebHostBuilderFactory.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.WebHostBuilderFactory; + +namespace Microsoft.AspNetCore.TestHost +{ + public static class WebHostBuilderFactory + { + public static IWebHostBuilder CreateFromAssemblyEntryPoint(Assembly assembly, string [] args) + { + var result = WebHostFactoryResolver.ResolveWebHostBuilderFactory<IWebHost,IWebHostBuilder>(assembly); + if (result.ResultKind != FactoryResolutionResultKind.Success) + { + return null; + } + + return result.WebHostBuilderFactory(args); + } + + public static IWebHostBuilder CreateFromTypesAssemblyEntryPoint<T>(string[] args) => + CreateFromAssemblyEntryPoint(typeof(T).Assembly, args); + } +} diff --git a/src/Hosting/TestHost/src/WebSocketClient.cs b/src/Hosting/TestHost/src/WebSocketClient.cs new file mode 100644 index 0000000000000000000000000000000000000000..e3deb670a5a2fb5b4d0ad6b0f8a26181a549aae5 --- /dev/null +++ b/src/Hosting/TestHost/src/WebSocketClient.cs @@ -0,0 +1,134 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.WebSockets; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Context = Microsoft.AspNetCore.Hosting.Internal.HostingApplication.Context; + +namespace Microsoft.AspNetCore.TestHost +{ + public class WebSocketClient + { + private readonly IHttpApplication<Context> _application; + private readonly PathString _pathBase; + + internal WebSocketClient(PathString pathBase, IHttpApplication<Context> application) + { + _application = application ?? throw new ArgumentNullException(nameof(application)); + + // PathString.StartsWithSegments that we use below requires the base path to not end in a slash. + if (pathBase.HasValue && pathBase.Value.EndsWith("/")) + { + pathBase = new PathString(pathBase.Value.Substring(0, pathBase.Value.Length - 1)); + } + _pathBase = pathBase; + + SubProtocols = new List<string>(); + } + + public IList<string> SubProtocols + { + get; + private set; + } + + public Action<HttpRequest> ConfigureRequest + { + get; + set; + } + + public async Task<WebSocket> ConnectAsync(Uri uri, CancellationToken cancellationToken) + { + WebSocketFeature webSocketFeature = null; + var contextBuilder = new HttpContextBuilder(_application); + contextBuilder.Configure(context => + { + var request = context.Request; + var scheme = uri.Scheme; + scheme = (scheme == "ws") ? "http" : scheme; + scheme = (scheme == "wss") ? "https" : scheme; + request.Scheme = scheme; + request.Path = PathString.FromUriComponent(uri); + request.PathBase = PathString.Empty; + if (request.Path.StartsWithSegments(_pathBase, out var remainder)) + { + request.Path = remainder; + request.PathBase = _pathBase; + } + request.QueryString = QueryString.FromUriComponent(uri); + request.Headers.Add("Connection", new string[] { "Upgrade" }); + request.Headers.Add("Upgrade", new string[] { "websocket" }); + request.Headers.Add("Sec-WebSocket-Version", new string[] { "13" }); + request.Headers.Add("Sec-WebSocket-Key", new string[] { CreateRequestKey() }); + request.Body = Stream.Null; + + // WebSocket + webSocketFeature = new WebSocketFeature(context); + context.Features.Set<IHttpWebSocketFeature>(webSocketFeature); + + ConfigureRequest?.Invoke(context.Request); + }); + + var httpContext = await contextBuilder.SendAsync(cancellationToken); + + if (httpContext.Response.StatusCode != StatusCodes.Status101SwitchingProtocols) + { + throw new InvalidOperationException("Incomplete handshake, status code: " + httpContext.Response.StatusCode); + } + if (webSocketFeature.ClientWebSocket == null) + { + throw new InvalidOperationException("Incomplete handshake"); + } + + return webSocketFeature.ClientWebSocket; + } + + private string CreateRequestKey() + { + byte[] data = new byte[16]; + var rng = RandomNumberGenerator.Create(); + rng.GetBytes(data); + return Convert.ToBase64String(data); + } + + private class WebSocketFeature : IHttpWebSocketFeature + { + private readonly HttpContext _httpContext; + + public WebSocketFeature(HttpContext context) + { + _httpContext = context; + } + + bool IHttpWebSocketFeature.IsWebSocketRequest => true; + + public WebSocket ClientWebSocket { get; private set; } + + public WebSocket ServerWebSocket { get; private set; } + + async Task<WebSocket> IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context) + { + var websockets = TestWebSocket.CreatePair(context.SubProtocol); + if (_httpContext.Response.HasStarted) + { + throw new InvalidOperationException("The response has already started"); + } + + _httpContext.Response.StatusCode = StatusCodes.Status101SwitchingProtocols; + ClientWebSocket = websockets.Item1; + ServerWebSocket = websockets.Item2; + await _httpContext.Response.Body.FlushAsync(_httpContext.RequestAborted); // Send headers to the client + return ServerWebSocket; + } + } + } +} \ No newline at end of file diff --git a/src/Hosting/TestHost/src/baseline.netcore.json b/src/Hosting/TestHost/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..85f67361ddca97cfd3bb0756ddebf96d733adfde --- /dev/null +++ b/src/Hosting/TestHost/src/baseline.netcore.json @@ -0,0 +1,316 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.TestHost, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.TestHost.ClientHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.Net.Http.HttpMessageHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "SendAsync", + "Parameters": [ + { + "Name": "request", + "Type": "System.Net.Http.HttpRequestMessage" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage>", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "pathBase", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "application", + "Type": "Microsoft.AspNetCore.Hosting.Server.IHttpApplication<Microsoft.AspNetCore.Hosting.Internal.HostingApplication+Context>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.TestHost.RequestBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "And", + "Parameters": [ + { + "Name": "configure", + "Type": "System.Action<System.Net.Http.HttpRequestMessage>" + } + ], + "ReturnType": "Microsoft.AspNetCore.TestHost.RequestBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddHeader", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.TestHost.RequestBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SendAsync", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "PostAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "server", + "Type": "Microsoft.AspNetCore.TestHost.TestServer" + }, + { + "Name": "path", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.TestHost.TestServer", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Hosting.Server.IServer" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_BaseAddress", + "Parameters": [], + "ReturnType": "System.Uri", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BaseAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.Uri" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Host", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHost", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Features", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.Server.IServer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateHandler", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpMessageHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateClient", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpClient", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateWebSocketClient", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.TestHost.WebSocketClient", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateRequest", + "Parameters": [ + { + "Name": "path", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.TestHost.RequestBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + }, + { + "Name": "featureCollection", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.TestHost.WebSocketClient", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_SubProtocols", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList<System.String>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConfigureRequest", + "Parameters": [], + "ReturnType": "System.Action<Microsoft.AspNetCore.Http.HttpRequest>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConfigureRequest", + "Parameters": [ + { + "Name": "value", + "Type": "System.Action<Microsoft.AspNetCore.Http.HttpRequest>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ConnectAsync", + "Parameters": [ + { + "Name": "uri", + "Type": "System.Uri" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Net.WebSockets.WebSocket>", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Hosting/TestHost/test/ClientHandlerTests.cs b/src/Hosting/TestHost/test/ClientHandlerTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..73f1c86d2973bc2e9c958ae9eb461ab6a72e430a --- /dev/null +++ b/src/Hosting/TestHost/test/ClientHandlerTests.cs @@ -0,0 +1,372 @@ +// 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.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; +using Context = Microsoft.AspNetCore.Hosting.Internal.HostingApplication.Context; + +namespace Microsoft.AspNetCore.TestHost +{ + public class ClientHandlerTests + { + [Fact] + public Task ExpectedKeysAreAvailable() + { + var handler = new ClientHandler(new PathString("/A/Path/"), new DummyApplication(context => + { + // TODO: Assert.True(context.RequestAborted.CanBeCanceled); +#if NETCOREAPP2_1 + Assert.Equal("HTTP/2.0", context.Request.Protocol); +#elif NET461 || NETCOREAPP2_0 + Assert.Equal("HTTP/1.1", context.Request.Protocol); +#else + Unspecified Framework +#endif + Assert.Equal("GET", context.Request.Method); + Assert.Equal("https", context.Request.Scheme); + Assert.Equal("/A/Path", context.Request.PathBase.Value); + Assert.Equal("/and/file.txt", context.Request.Path.Value); + Assert.Equal("?and=query", context.Request.QueryString.Value); + Assert.NotNull(context.Request.Body); + Assert.NotNull(context.Request.Headers); + Assert.NotNull(context.Response.Headers); + Assert.NotNull(context.Response.Body); + Assert.Equal(200, context.Response.StatusCode); + Assert.Null(context.Features.Get<IHttpResponseFeature>().ReasonPhrase); + Assert.Equal("example.com", context.Request.Host.Value); + + return Task.FromResult(0); + })); + var httpClient = new HttpClient(handler); + return httpClient.GetAsync("https://example.com/A/Path/and/file.txt?and=query"); + } + + [Fact] + public Task ExpectedKeysAreInFeatures() + { + var handler = new ClientHandler(new PathString("/A/Path/"), new InspectingApplication(features => + { + // TODO: Assert.True(context.RequestAborted.CanBeCanceled); +#if NETCOREAPP2_1 + Assert.Equal("HTTP/2.0", features.Get<IHttpRequestFeature>().Protocol); +#elif NET461 || NETCOREAPP2_0 + Assert.Equal("HTTP/1.1", features.Get<IHttpRequestFeature>().Protocol); +#else + Unspecified Framework +#endif + Assert.Equal("GET", features.Get<IHttpRequestFeature>().Method); + Assert.Equal("https", features.Get<IHttpRequestFeature>().Scheme); + Assert.Equal("/A/Path", features.Get<IHttpRequestFeature>().PathBase); + Assert.Equal("/and/file.txt", features.Get<IHttpRequestFeature>().Path); + Assert.Equal("?and=query", features.Get<IHttpRequestFeature>().QueryString); + Assert.NotNull(features.Get<IHttpRequestFeature>().Body); + Assert.NotNull(features.Get<IHttpRequestFeature>().Headers); + Assert.NotNull(features.Get<IHttpResponseFeature>().Headers); + Assert.NotNull(features.Get<IHttpResponseFeature>().Body); + Assert.Equal(200, features.Get<IHttpResponseFeature>().StatusCode); + Assert.Null(features.Get<IHttpResponseFeature>().ReasonPhrase); + Assert.Equal("example.com", features.Get<IHttpRequestFeature>().Headers["host"]); + Assert.NotNull(features.Get<IHttpRequestLifetimeFeature>()); + })); + var httpClient = new HttpClient(handler); + return httpClient.GetAsync("https://example.com/A/Path/and/file.txt?and=query"); + } + + [Fact] + public Task SingleSlashNotMovedToPathBase() + { + var handler = new ClientHandler(new PathString(""), new DummyApplication(context => + { + Assert.Equal("", context.Request.PathBase.Value); + Assert.Equal("/", context.Request.Path.Value); + + return Task.FromResult(0); + })); + var httpClient = new HttpClient(handler); + return httpClient.GetAsync("https://example.com/"); + } + + [Fact] + public async Task ResubmitRequestWorks() + { + int requestCount = 1; + var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => + { + int read = context.Request.Body.Read(new byte[100], 0, 100); + Assert.Equal(11, read); + + context.Response.Headers["TestHeader"] = "TestValue:" + requestCount++; + return Task.FromResult(0); + })); + + HttpMessageInvoker invoker = new HttpMessageInvoker(handler); + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, "https://example.com/"); + message.Content = new StringContent("Hello World"); + + HttpResponseMessage response = await invoker.SendAsync(message, CancellationToken.None); + Assert.Equal("TestValue:1", response.Headers.GetValues("TestHeader").First()); + + response = await invoker.SendAsync(message, CancellationToken.None); + Assert.Equal("TestValue:2", response.Headers.GetValues("TestHeader").First()); + } + + [Fact] + public async Task MiddlewareOnlySetsHeaders() + { + var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => + { + context.Response.Headers["TestHeader"] = "TestValue"; + return Task.FromResult(0); + })); + var httpClient = new HttpClient(handler); + HttpResponseMessage response = await httpClient.GetAsync("https://example.com/"); + Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); + } + + [Fact] + public async Task BlockingMiddlewareShouldNotBlockClient() + { + ManualResetEvent block = new ManualResetEvent(false); + var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => + { + block.WaitOne(); + return Task.FromResult(0); + })); + var httpClient = new HttpClient(handler); + Task<HttpResponseMessage> task = httpClient.GetAsync("https://example.com/"); + Assert.False(task.IsCompleted); + Assert.False(task.Wait(50)); + block.Set(); + HttpResponseMessage response = await task; + } + + [Fact] + public async Task HeadersAvailableBeforeBodyFinished() + { + ManualResetEvent block = new ManualResetEvent(false); + var handler = new ClientHandler(PathString.Empty, new DummyApplication(async context => + { + context.Response.Headers["TestHeader"] = "TestValue"; + await context.Response.WriteAsync("BodyStarted,"); + block.WaitOne(); + await context.Response.WriteAsync("BodyFinished"); + })); + var httpClient = new HttpClient(handler); + HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead); + Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); + block.Set(); + Assert.Equal("BodyStarted,BodyFinished", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task FlushSendsHeaders() + { + ManualResetEvent block = new ManualResetEvent(false); + var handler = new ClientHandler(PathString.Empty, new DummyApplication(async context => + { + context.Response.Headers["TestHeader"] = "TestValue"; + context.Response.Body.Flush(); + block.WaitOne(); + await context.Response.WriteAsync("BodyFinished"); + })); + var httpClient = new HttpClient(handler); + HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead); + Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); + block.Set(); + Assert.Equal("BodyFinished", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task ClientDisposalCloses() + { + ManualResetEvent block = new ManualResetEvent(false); + var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => + { + context.Response.Headers["TestHeader"] = "TestValue"; + context.Response.Body.Flush(); + block.WaitOne(); + return Task.FromResult(0); + })); + var httpClient = new HttpClient(handler); + HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead); + Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); + Stream responseStream = await response.Content.ReadAsStreamAsync(); + Task<int> readTask = responseStream.ReadAsync(new byte[100], 0, 100); + Assert.False(readTask.IsCompleted); + responseStream.Dispose(); + Assert.True(readTask.Wait(TimeSpan.FromSeconds(10)), "Finished"); + Assert.Equal(0, readTask.Result); + block.Set(); + } + + [Fact] + public async Task ClientCancellationAborts() + { + ManualResetEvent block = new ManualResetEvent(false); + var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => + { + context.Response.Headers["TestHeader"] = "TestValue"; + context.Response.Body.Flush(); + block.WaitOne(); + return Task.FromResult(0); + })); + var httpClient = new HttpClient(handler); + HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead); + Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); + Stream responseStream = await response.Content.ReadAsStreamAsync(); + CancellationTokenSource cts = new CancellationTokenSource(); + Task<int> readTask = responseStream.ReadAsync(new byte[100], 0, 100, cts.Token); + Assert.False(readTask.IsCompleted, "Not Completed"); + cts.Cancel(); + var ex = Assert.Throws<AggregateException>(() => readTask.Wait(TimeSpan.FromSeconds(10))); + Assert.IsAssignableFrom<OperationCanceledException>(ex.GetBaseException()); + block.Set(); + } + + [Fact] + public Task ExceptionBeforeFirstWriteIsReported() + { + var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => + { + throw new InvalidOperationException("Test Exception"); + })); + var httpClient = new HttpClient(handler); + return Assert.ThrowsAsync<InvalidOperationException>(() => httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead)); + } + + [Fact] + public async Task ExceptionAfterFirstWriteIsReported() + { + ManualResetEvent block = new ManualResetEvent(false); + var handler = new ClientHandler(PathString.Empty, new DummyApplication(async context => + { + context.Response.Headers["TestHeader"] = "TestValue"; + await context.Response.WriteAsync("BodyStarted"); + block.WaitOne(); + throw new InvalidOperationException("Test Exception"); + })); + var httpClient = new HttpClient(handler); + HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead); + Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); + block.Set(); + var ex = await Assert.ThrowsAsync<HttpRequestException>(() => response.Content.ReadAsStringAsync()); + Assert.IsType<InvalidOperationException>(ex.GetBaseException()); + } + + private class DummyApplication : IHttpApplication<Context> + { + RequestDelegate _application; + + public DummyApplication(RequestDelegate application) + { + _application = application; + } + + public Context CreateContext(IFeatureCollection contextFeatures) + { + return new Context() + { + HttpContext = new DefaultHttpContext(contextFeatures) + }; + } + + public void DisposeContext(Context context, Exception exception) + { + + } + + public Task ProcessRequestAsync(Context context) + { + return _application(context.HttpContext); + } + } + + private class InspectingApplication : IHttpApplication<Context> + { + Action<IFeatureCollection> _inspector; + + public InspectingApplication(Action<IFeatureCollection> inspector) + { + _inspector = inspector; + } + + public Context CreateContext(IFeatureCollection contextFeatures) + { + _inspector(contextFeatures); + return new Context() + { + HttpContext = new DefaultHttpContext(contextFeatures) + }; + } + + public void DisposeContext(Context context, Exception exception) + { + + } + + public Task ProcessRequestAsync(Context context) + { + return Task.FromResult(0); + } + } + + [Fact] + public async Task ClientHandlerCreateContextWithDefaultRequestParameters() + { + // This logger will attempt to access information from HttpRequest once the HttpContext is created + var logger = new VerifierLogger(); + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddSingleton<ILogger<IWebHost>>(logger); + }) + .Configure(app => + { + app.Run(context => + { + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + // The HttpContext will be created and the logger will make sure that the HttpRequest exists and contains reasonable values + var result = await server.CreateClient().GetStringAsync("/"); + } + + private class VerifierLogger : ILogger<IWebHost> + { + public IDisposable BeginScope<TState>(TState state) => new NoopDispoasble(); + + public bool IsEnabled(LogLevel logLevel) => true; + + // This call verifies that fields of HttpRequest are accessed and valid + public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) => formatter(state, exception); + + class NoopDispoasble : IDisposable + { + public void Dispose() + { + } + } + } + } +} diff --git a/src/Hosting/TestHost/test/HttpContextBuilderTests.cs b/src/Hosting/TestHost/test/HttpContextBuilderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..21539c8988e214bac11966bfd647db9a34c73f26 --- /dev/null +++ b/src/Hosting/TestHost/test/HttpContextBuilderTests.cs @@ -0,0 +1,332 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.TestHost +{ + public class HttpContextBuilderTests + { + [Fact] + public async Task ExpectedValuesAreAvailable() + { + var builder = new WebHostBuilder().Configure(app => { }); + var server = new TestServer(builder); + server.BaseAddress = new Uri("https://example.com/A/Path/"); + var context = await server.SendAsync(c => + { + c.Request.Method = HttpMethods.Post; + c.Request.Path = "/and/file.txt"; + c.Request.QueryString = new QueryString("?and=query"); + }); + + Assert.True(context.RequestAborted.CanBeCanceled); + Assert.Equal("HTTP/1.1", context.Request.Protocol); + Assert.Equal("POST", context.Request.Method); + Assert.Equal("https", context.Request.Scheme); + Assert.Equal("example.com", context.Request.Host.Value); + Assert.Equal("/A/Path", context.Request.PathBase.Value); + Assert.Equal("/and/file.txt", context.Request.Path.Value); + Assert.Equal("?and=query", context.Request.QueryString.Value); + Assert.NotNull(context.Request.Body); + Assert.NotNull(context.Request.Headers); + Assert.NotNull(context.Response.Headers); + Assert.NotNull(context.Response.Body); + Assert.Equal(404, context.Response.StatusCode); + Assert.Null(context.Features.Get<IHttpResponseFeature>().ReasonPhrase); + } + + [Fact] + public async Task SingleSlashNotMovedToPathBase() + { + var builder = new WebHostBuilder().Configure(app => { }); + var server = new TestServer(builder); + var context = await server.SendAsync(c => + { + c.Request.Path = "/"; + }); + + Assert.Equal("", context.Request.PathBase.Value); + Assert.Equal("/", context.Request.Path.Value); + } + + [Fact] + public async Task MiddlewareOnlySetsHeaders() + { + var builder = new WebHostBuilder().Configure(app => + { + app.Run(c => + { + c.Response.Headers["TestHeader"] = "TestValue"; + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + var context = await server.SendAsync(c => { }); + + Assert.Equal("TestValue", context.Response.Headers["TestHeader"]); + } + + [Fact] + public async Task BlockingMiddlewareShouldNotBlockClient() + { + var block = new ManualResetEvent(false); + var builder = new WebHostBuilder().Configure(app => + { + app.Run(c => + { + block.WaitOne(); + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + var task = server.SendAsync(c => { }); + + Assert.False(task.IsCompleted); + Assert.False(task.Wait(50)); + block.Set(); + var context = await task; + } + + [Fact] + public async Task HeadersAvailableBeforeSyncBodyFinished() + { + var block = new ManualResetEvent(false); + var builder = new WebHostBuilder().Configure(app => + { + app.Run(c => + { + c.Response.Headers["TestHeader"] = "TestValue"; + var bytes = Encoding.UTF8.GetBytes("BodyStarted" + Environment.NewLine); + c.Response.Body.Write(bytes, 0, bytes.Length); + Assert.True(block.WaitOne(TimeSpan.FromSeconds(5))); + bytes = Encoding.UTF8.GetBytes("BodyFinished"); + c.Response.Body.Write(bytes, 0, bytes.Length); + return Task.CompletedTask; + }); + }); + var server = new TestServer(builder); + var context = await server.SendAsync(c => { }); + + Assert.Equal("TestValue", context.Response.Headers["TestHeader"]); + var reader = new StreamReader(context.Response.Body); + Assert.Equal("BodyStarted", reader.ReadLine()); + block.Set(); + Assert.Equal("BodyFinished", reader.ReadToEnd()); + } + + [Fact] + public async Task HeadersAvailableBeforeAsyncBodyFinished() + { + var block = new ManualResetEvent(false); + var builder = new WebHostBuilder().Configure(app => + { + app.Run(async c => + { + c.Response.Headers["TestHeader"] = "TestValue"; + await c.Response.WriteAsync("BodyStarted" + Environment.NewLine); + Assert.True(block.WaitOne(TimeSpan.FromSeconds(5))); + await c.Response.WriteAsync("BodyFinished"); + }); + }); + var server = new TestServer(builder); + var context = await server.SendAsync(c => { }); + + Assert.Equal("TestValue", context.Response.Headers["TestHeader"]); + var reader = new StreamReader(context.Response.Body); + Assert.Equal("BodyStarted", await reader.ReadLineAsync()); + block.Set(); + Assert.Equal("BodyFinished", await reader.ReadToEndAsync()); + } + + [Fact] + public async Task FlushSendsHeaders() + { + var block = new ManualResetEvent(false); + var builder = new WebHostBuilder().Configure(app => + { + app.Run(async c => + { + c.Response.Headers["TestHeader"] = "TestValue"; + c.Response.Body.Flush(); + block.WaitOne(); + await c.Response.WriteAsync("BodyFinished"); + }); + }); + var server = new TestServer(builder); + var context = await server.SendAsync(c => { }); + + Assert.Equal("TestValue", context.Response.Headers["TestHeader"]); + block.Set(); + Assert.Equal("BodyFinished", new StreamReader(context.Response.Body).ReadToEnd()); + } + + [Fact] + public async Task ClientDisposalCloses() + { + var block = new ManualResetEvent(false); + var builder = new WebHostBuilder().Configure(app => + { + app.Run(async c => + { + c.Response.Headers["TestHeader"] = "TestValue"; + c.Response.Body.Flush(); + block.WaitOne(); + await c.Response.WriteAsync("BodyFinished"); + }); + }); + var server = new TestServer(builder); + var context = await server.SendAsync(c => { }); + + Assert.Equal("TestValue", context.Response.Headers["TestHeader"]); + var responseStream = context.Response.Body; + Task<int> readTask = responseStream.ReadAsync(new byte[100], 0, 100); + Assert.False(readTask.IsCompleted); + responseStream.Dispose(); + Assert.True(readTask.Wait(TimeSpan.FromSeconds(10))); + Assert.Equal(0, readTask.Result); + block.Set(); + } + + [Fact] + public void ClientCancellationAborts() + { + var block = new ManualResetEvent(false); + var builder = new WebHostBuilder().Configure(app => + { + app.Run(c => + { + block.Set(); + Assert.True(c.RequestAborted.WaitHandle.WaitOne(TimeSpan.FromSeconds(10))); + c.RequestAborted.ThrowIfCancellationRequested(); + return Task.CompletedTask; + }); + }); + var server = new TestServer(builder); + var cts = new CancellationTokenSource(); + var contextTask = server.SendAsync(c => { }, cts.Token); + block.WaitOne(); + cts.Cancel(); + + var ex = Assert.Throws<AggregateException>(() => contextTask.Wait(TimeSpan.FromSeconds(10))); + Assert.IsAssignableFrom<OperationCanceledException>(ex.GetBaseException()); + } + + [Fact] + public async Task ClientCancellationAbortsReadAsync() + { + var block = new ManualResetEvent(false); + var builder = new WebHostBuilder().Configure(app => + { + app.Run(async c => + { + c.Response.Headers["TestHeader"] = "TestValue"; + c.Response.Body.Flush(); + block.WaitOne(); + await c.Response.WriteAsync("BodyFinished"); + }); + }); + var server = new TestServer(builder); + var context = await server.SendAsync(c => { }); + + Assert.Equal("TestValue", context.Response.Headers["TestHeader"]); + var responseStream = context.Response.Body; + var cts = new CancellationTokenSource(); + var readTask = responseStream.ReadAsync(new byte[100], 0, 100, cts.Token); + Assert.False(readTask.IsCompleted); + cts.Cancel(); + var ex = Assert.Throws<AggregateException>(() => readTask.Wait(TimeSpan.FromSeconds(10))); + Assert.IsAssignableFrom<OperationCanceledException>(ex.GetBaseException()); + block.Set(); + } + + [Fact] + public Task ExceptionBeforeFirstWriteIsReported() + { + var builder = new WebHostBuilder().Configure(app => + { + app.Run(c => + { + throw new InvalidOperationException("Test Exception"); + }); + }); + var server = new TestServer(builder); + return Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync(c => { })); + } + + [Fact] + public async Task ExceptionAfterFirstWriteIsReported() + { + var block = new ManualResetEvent(false); + var builder = new WebHostBuilder().Configure(app => + { + app.Run(async c => + { + c.Response.Headers["TestHeader"] = "TestValue"; + await c.Response.WriteAsync("BodyStarted"); + block.WaitOne(); + throw new InvalidOperationException("Test Exception"); + }); + }); + var server = new TestServer(builder); + var context = await server.SendAsync(c => { }); + + Assert.Equal("TestValue", context.Response.Headers["TestHeader"]); + Assert.Equal(11, context.Response.Body.Read(new byte[100], 0, 100)); + block.Set(); + var ex = Assert.Throws<IOException>(() => context.Response.Body.Read(new byte[100], 0, 100)); + Assert.IsAssignableFrom<InvalidOperationException>(ex.InnerException); + } + + [Fact] + public async Task ClientHandlerCreateContextWithDefaultRequestParameters() + { + // This logger will attempt to access information from HttpRequest once the HttpContext is created + var logger = new VerifierLogger(); + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddSingleton<ILogger<IWebHost>>(logger); + }) + .Configure(app => + { + app.Run(context => + { + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + // The HttpContext will be created and the logger will make sure that the HttpRequest exists and contains reasonable values + var ctx = await server.SendAsync(c => { }); + } + + private class VerifierLogger : ILogger<IWebHost> + { + public IDisposable BeginScope<TState>(TState state) => new NoopDispoasble(); + + public bool IsEnabled(LogLevel logLevel) => true; + + // This call verifies that fields of HttpRequest are accessed and valid + public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) => formatter(state, exception); + + class NoopDispoasble : IDisposable + { + public void Dispose() + { + } + } + } + } +} diff --git a/src/Hosting/TestHost/test/Microsoft.AspNetCore.TestHost.Tests.csproj b/src/Hosting/TestHost/test/Microsoft.AspNetCore.TestHost.Tests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..51041799c4824279ffe37758e8261975aefc406b --- /dev/null +++ b/src/Hosting/TestHost/test/Microsoft.AspNetCore.TestHost.Tests.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.TestHost" /> + <Reference Include="Microsoft.Extensions.DiagnosticAdapter" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/TestHost/test/RequestBuilderTests.cs b/src/Hosting/TestHost/test/RequestBuilderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..2a37015aae1aea25f4722f9d7c38ac09b8c65898 --- /dev/null +++ b/src/Hosting/TestHost/test/RequestBuilderTests.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Hosting; +using Xunit; + +namespace Microsoft.AspNetCore.TestHost +{ + public class RequestBuilderTests + { + [Fact] + public void AddRequestHeader() + { + var builder = new WebHostBuilder().Configure(app => { }); + var server = new TestServer(builder); + server.CreateRequest("/") + .AddHeader("Host", "MyHost:90") + .And(request => + { + Assert.Equal("MyHost:90", request.Headers.Host.ToString()); + }); + } + + [Fact] + public void AddContentHeaders() + { + var builder = new WebHostBuilder().Configure(app => { }); + var server = new TestServer(builder); + server.CreateRequest("/") + .AddHeader("Content-Type", "Test/Value") + .And(request => + { + Assert.NotNull(request.Content); + Assert.Equal("Test/Value", request.Content.Headers.ContentType.ToString()); + }); + } + } +} diff --git a/src/Hosting/TestHost/test/ResponseFeatureTests.cs b/src/Hosting/TestHost/test/ResponseFeatureTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..f8af4cf64d2469ee0133dee048aa13b5759a5d1c --- /dev/null +++ b/src/Hosting/TestHost/test/ResponseFeatureTests.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.TestHost +{ + public class ResponseFeatureTests + { + [Fact] + public async Task StatusCode_DefaultsTo200() + { + // Arrange & Act + var responseInformation = new ResponseFeature(); + + // Assert + Assert.Equal(200, responseInformation.StatusCode); + Assert.False(responseInformation.HasStarted); + + await responseInformation.FireOnSendingHeadersAsync(); + + Assert.True(responseInformation.HasStarted); + Assert.True(responseInformation.Headers.IsReadOnly); + } + + [Fact] + public void OnStarting_ThrowsWhenHasStarted() + { + // Arrange + var responseInformation = new ResponseFeature(); + responseInformation.HasStarted = true; + + // Act & Assert + Assert.Throws<InvalidOperationException>(() => + { + responseInformation.OnStarting((status) => + { + return Task.FromResult(string.Empty); + }, state: "string"); + }); + } + + [Fact] + public void StatusCode_ThrowsWhenHasStarted() + { + var responseInformation = new ResponseFeature(); + responseInformation.HasStarted = true; + + Assert.Throws<InvalidOperationException>(() => responseInformation.StatusCode = 400); + Assert.Throws<InvalidOperationException>(() => responseInformation.ReasonPhrase = "Hello World"); + } + + [Fact] + public void StatusCode_MustBeGreaterThan99() + { + var responseInformation = new ResponseFeature(); + + Assert.Throws<ArgumentOutOfRangeException>(() => responseInformation.StatusCode = 99); + Assert.Throws<ArgumentOutOfRangeException>(() => responseInformation.StatusCode = 0); + Assert.Throws<ArgumentOutOfRangeException>(() => responseInformation.StatusCode = -200); + responseInformation.StatusCode = 100; + responseInformation.StatusCode = 200; + responseInformation.StatusCode = 1000; + } + } +} \ No newline at end of file diff --git a/src/Hosting/TestHost/test/TestClientTests.cs b/src/Hosting/TestHost/test/TestClientTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..3101c2965f2d7207ef027e745e9176f8c6b76cd5 --- /dev/null +++ b/src/Hosting/TestHost/test/TestClientTests.cs @@ -0,0 +1,429 @@ +// 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.Linq; +using System.Net.Http; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.TestHost +{ + public class TestClientTests + { + [Fact] + public async Task GetAsyncWorks() + { + // Arrange + var expected = "GET Response"; + RequestDelegate appDelegate = ctx => + ctx.Response.WriteAsync(expected); + var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act + var actual = await client.GetStringAsync("http://localhost:12345"); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + public async Task NoTrailingSlash_NoPathBase() + { + // Arrange + var expected = "GET Response"; + RequestDelegate appDelegate = ctx => + { + Assert.Equal("", ctx.Request.PathBase.Value); + Assert.Equal("/", ctx.Request.Path.Value); + return ctx.Response.WriteAsync(expected); + }; + var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act + var actual = await client.GetStringAsync("http://localhost:12345"); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + public async Task SingleTrailingSlash_NoPathBase() + { + // Arrange + var expected = "GET Response"; + RequestDelegate appDelegate = ctx => + { + Assert.Equal("", ctx.Request.PathBase.Value); + Assert.Equal("/", ctx.Request.Path.Value); + return ctx.Response.WriteAsync(expected); + }; + var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act + var actual = await client.GetStringAsync("http://localhost:12345/"); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + public async Task PutAsyncWorks() + { + // Arrange + RequestDelegate appDelegate = ctx => + ctx.Response.WriteAsync(new StreamReader(ctx.Request.Body).ReadToEnd() + " PUT Response"); + var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act + var content = new StringContent("Hello world"); + var response = await client.PutAsync("http://localhost:12345", content); + + // Assert + Assert.Equal("Hello world PUT Response", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task PostAsyncWorks() + { + // Arrange + RequestDelegate appDelegate = async ctx => + await ctx.Response.WriteAsync(new StreamReader(ctx.Request.Body).ReadToEnd() + " POST Response"); + var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act + var content = new StringContent("Hello world"); + var response = await client.PostAsync("http://localhost:12345", content); + + // Assert + Assert.Equal("Hello world POST Response", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task LargePayload_DisposesRequest_AfterResponseIsCompleted() + { + // Arrange + var data = new byte[2048]; + var character = Encoding.ASCII.GetBytes("a"); + + for (var i = 0; i < data.Length; i++) + { + data[i] = character[0]; + } + + var builder = new WebHostBuilder(); + RequestDelegate app = (ctx) => + { + var disposable = new TestDisposable(); + ctx.Response.RegisterForDispose(disposable); + ctx.Response.Body.Write(data, 0, 1024); + + Assert.False(disposable.IsDisposed); + + ctx.Response.Body.Write(data, 1024, 1024); + return Task.FromResult(0); + }; + + builder.Configure(appBuilder => appBuilder.Run(app)); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act & Assert + var response = await client.GetAsync("http://localhost:12345"); + } + + private class TestDisposable : IDisposable + { + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + } + } + + [Fact] + public async Task WebSocketWorks() + { + // Arrange + // This logger will attempt to access information from HttpRequest once the HttpContext is created + var logger = new VerifierLogger(); + RequestDelegate appDelegate = async ctx => + { + if (ctx.WebSockets.IsWebSocketRequest) + { + var websocket = await ctx.WebSockets.AcceptWebSocketAsync(); + var receiveArray = new byte[1024]; + while (true) + { + var receiveResult = await websocket.ReceiveAsync(new System.ArraySegment<byte>(receiveArray), CancellationToken.None); + if (receiveResult.MessageType == WebSocketMessageType.Close) + { + await websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Normal Closure", CancellationToken.None); + break; + } + else + { + var sendBuffer = new System.ArraySegment<byte>(receiveArray, 0, receiveResult.Count); + await websocket.SendAsync(sendBuffer, receiveResult.MessageType, receiveResult.EndOfMessage, CancellationToken.None); + } + } + } + }; + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddSingleton<ILogger<IWebHost>>(logger); + }) + .Configure(app => + { + app.Run(appDelegate); + }); + var server = new TestServer(builder); + + // Act + var client = server.CreateWebSocketClient(); + // The HttpContext will be created and the logger will make sure that the HttpRequest exists and contains reasonable values + var clientSocket = await client.ConnectAsync(new System.Uri("http://localhost"), CancellationToken.None); + var hello = Encoding.UTF8.GetBytes("hello"); + await clientSocket.SendAsync(new System.ArraySegment<byte>(hello), WebSocketMessageType.Text, true, CancellationToken.None); + var world = Encoding.UTF8.GetBytes("world!"); + await clientSocket.SendAsync(new System.ArraySegment<byte>(world), WebSocketMessageType.Binary, true, CancellationToken.None); + await clientSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Normal Closure", CancellationToken.None); + + // Assert + Assert.Equal(WebSocketState.CloseSent, clientSocket.State); + + var buffer = new byte[1024]; + var result = await clientSocket.ReceiveAsync(new System.ArraySegment<byte>(buffer), CancellationToken.None); + Assert.Equal(hello.Length, result.Count); + Assert.True(hello.SequenceEqual(buffer.Take(hello.Length))); + Assert.Equal(WebSocketMessageType.Text, result.MessageType); + + result = await clientSocket.ReceiveAsync(new System.ArraySegment<byte>(buffer), CancellationToken.None); + Assert.Equal(world.Length, result.Count); + Assert.True(world.SequenceEqual(buffer.Take(world.Length))); + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + + result = await clientSocket.ReceiveAsync(new System.ArraySegment<byte>(buffer), CancellationToken.None); + Assert.Equal(WebSocketMessageType.Close, result.MessageType); + Assert.Equal(WebSocketState.Closed, clientSocket.State); + + clientSocket.Dispose(); + } + + [ConditionalFact] + public async Task WebSocketAcceptThrowsWhenCancelled() + { + // Arrange + // This logger will attempt to access information from HttpRequest once the HttpContext is created + var logger = new VerifierLogger(); + RequestDelegate appDelegate = async ctx => + { + if (ctx.WebSockets.IsWebSocketRequest) + { + var websocket = await ctx.WebSockets.AcceptWebSocketAsync(); + var receiveArray = new byte[1024]; + while (true) + { + var receiveResult = await websocket.ReceiveAsync(new ArraySegment<byte>(receiveArray), CancellationToken.None); + if (receiveResult.MessageType == WebSocketMessageType.Close) + { + await websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Normal Closure", CancellationToken.None); + break; + } + else + { + var sendBuffer = new System.ArraySegment<byte>(receiveArray, 0, receiveResult.Count); + await websocket.SendAsync(sendBuffer, receiveResult.MessageType, receiveResult.EndOfMessage, CancellationToken.None); + } + } + } + }; + var builder = new WebHostBuilder() + .ConfigureServices(services => services.AddSingleton<ILogger<IWebHost>>(logger)) + .Configure(app => app.Run(appDelegate)); + var server = new TestServer(builder); + + // Act + var client = server.CreateWebSocketClient(); + var tokenSource = new CancellationTokenSource(); + tokenSource.Cancel(); + + // Assert + await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await client.ConnectAsync(new Uri("http://localhost"), tokenSource.Token)); + } + + private class VerifierLogger : ILogger<IWebHost> + { + public IDisposable BeginScope<TState>(TState state) => new NoopDispoasble(); + + public bool IsEnabled(LogLevel logLevel) => true; + + // This call verifies that fields of HttpRequest are accessed and valid + public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) => formatter(state, exception); + + class NoopDispoasble : IDisposable + { + public void Dispose() + { + } + } + } + + [Fact] + public async Task WebSocketDisposalThrowsOnPeer() + { + // Arrange + RequestDelegate appDelegate = async ctx => + { + if (ctx.WebSockets.IsWebSocketRequest) + { + var websocket = await ctx.WebSockets.AcceptWebSocketAsync(); + websocket.Dispose(); + } + }; + var builder = new WebHostBuilder().Configure(app => + { + app.Run(appDelegate); + }); + var server = new TestServer(builder); + + // Act + var client = server.CreateWebSocketClient(); + var clientSocket = await client.ConnectAsync(new System.Uri("http://localhost"), CancellationToken.None); + var buffer = new byte[1024]; + await Assert.ThrowsAsync<IOException>(async () => await clientSocket.ReceiveAsync(new System.ArraySegment<byte>(buffer), CancellationToken.None)); + + clientSocket.Dispose(); + } + + [Fact] + public async Task WebSocketTinyReceiveGeneratesEndOfMessage() + { + // Arrange + RequestDelegate appDelegate = async ctx => + { + if (ctx.WebSockets.IsWebSocketRequest) + { + var websocket = await ctx.WebSockets.AcceptWebSocketAsync(); + var receiveArray = new byte[1024]; + while (true) + { + var receiveResult = await websocket.ReceiveAsync(new System.ArraySegment<byte>(receiveArray), CancellationToken.None); + var sendBuffer = new System.ArraySegment<byte>(receiveArray, 0, receiveResult.Count); + await websocket.SendAsync(sendBuffer, receiveResult.MessageType, receiveResult.EndOfMessage, CancellationToken.None); + } + } + }; + var builder = new WebHostBuilder().Configure(app => + { + app.Run(appDelegate); + }); + var server = new TestServer(builder); + + // Act + var client = server.CreateWebSocketClient(); + var clientSocket = await client.ConnectAsync(new System.Uri("http://localhost"), CancellationToken.None); + var hello = Encoding.UTF8.GetBytes("hello"); + await clientSocket.SendAsync(new System.ArraySegment<byte>(hello), WebSocketMessageType.Text, true, CancellationToken.None); + + // Assert + var buffer = new byte[1]; + for (var i = 0; i < hello.Length; i++) + { + bool last = i == (hello.Length - 1); + var result = await clientSocket.ReceiveAsync(new System.ArraySegment<byte>(buffer), CancellationToken.None); + Assert.Equal(buffer.Length, result.Count); + Assert.Equal(buffer[0], hello[i]); + Assert.Equal(last, result.EndOfMessage); + } + + clientSocket.Dispose(); + } + + [Fact] + public async Task ClientDisposalAbortsRequest() + { + // Arrange + TaskCompletionSource<object> tcs = new TaskCompletionSource<object>(); + RequestDelegate appDelegate = async ctx => + { + // Write Headers + await ctx.Response.Body.FlushAsync(); + + var sem = new SemaphoreSlim(0); + try + { + await sem.WaitAsync(ctx.RequestAborted); + } + catch (Exception e) + { + tcs.SetException(e); + } + }; + + // Act + var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:12345"); + var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + // Abort Request + response.Dispose(); + + // Assert + var exception = await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await tcs.Task); + } + + [Fact] + public async Task ClientCancellationAbortsRequest() + { + // Arrange + TaskCompletionSource<object> tcs = new TaskCompletionSource<object>(); + RequestDelegate appDelegate = async ctx => + { + var sem = new SemaphoreSlim(0); + try + { + await sem.WaitAsync(ctx.RequestAborted); + } + catch (Exception e) + { + tcs.SetException(e); + } + }; + + // Act + var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); + var server = new TestServer(builder); + var client = server.CreateClient(); + var cts = new CancellationTokenSource(); + cts.CancelAfter(500); + var response = await client.GetAsync("http://localhost:12345", cts.Token); + + // Assert + var exception = await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await tcs.Task); + } + } +} diff --git a/src/Hosting/TestHost/test/TestServerTests.cs b/src/Hosting/TestHost/test/TestServerTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..b1eee1fd9495973cd57df121dbf7e890cdf7cc01 --- /dev/null +++ b/src/Hosting/TestHost/test/TestServerTests.cs @@ -0,0 +1,687 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DiagnosticAdapter; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.TestHost +{ + public class TestServerTests + { + [Fact] + public void CreateWithDelegate() + { + // Arrange + // Act & Assert (Does not throw) + new TestServer(new WebHostBuilder().Configure(app => { })); + } + + [Fact] + public void DoesNotCaptureStartupErrorsByDefault() + { + var builder = new WebHostBuilder() + .Configure(app => + { + throw new InvalidOperationException(); + }); + + Assert.Throws<InvalidOperationException>(() => new TestServer(builder)); + } + + [Fact] + public async Task ServicesCanBeOverridenForTestingAsync() + { + var builder = new WebHostBuilder() + .ConfigureServices(s => s.AddSingleton<IServiceProviderFactory<ThirdPartyContainer>, ThirdPartyContainerServiceProviderFactory>()) + .UseStartup<ThirdPartyContainerStartup>() + .ConfigureTestServices(services => services.AddSingleton(new SimpleService { Message = "OverridesConfigureServices" })) + .ConfigureTestContainer<ThirdPartyContainer>(container => container.Services.AddSingleton(new TestService { Message = "OverridesConfigureContainer" })); + + var host = new TestServer(builder); + + var response = await host.CreateClient().GetStringAsync("/"); + + Assert.Equal("OverridesConfigureServices, OverridesConfigureContainer", response); + } + + public class ThirdPartyContainerStartup + { + public void ConfigureServices(IServiceCollection services) => + services.AddSingleton(new SimpleService { Message = "ConfigureServices" }); + + public void ConfigureContainer(ThirdPartyContainer container) => + container.Services.AddSingleton(new TestService { Message = "ConfigureContainer" }); + + public void Configure(IApplicationBuilder app) => + app.Use((ctx, next) => ctx.Response.WriteAsync( + $"{ctx.RequestServices.GetRequiredService<SimpleService>().Message}, {ctx.RequestServices.GetRequiredService<TestService>().Message}")); + } + + public class ThirdPartyContainer + { + public IServiceCollection Services { get; set; } + } + + public class ThirdPartyContainerServiceProviderFactory : IServiceProviderFactory<ThirdPartyContainer> + { + public ThirdPartyContainer CreateBuilder(IServiceCollection services) => new ThirdPartyContainer { Services = services }; + + public IServiceProvider CreateServiceProvider(ThirdPartyContainer containerBuilder) => containerBuilder.Services.BuildServiceProvider(); + } + + [Fact] + public void CaptureStartupErrorsSettingPreserved() + { + var builder = new WebHostBuilder() + .CaptureStartupErrors(true) + .Configure(app => + { + throw new InvalidOperationException(); + }); + + // Does not throw + new TestServer(builder); + } + + [Fact] + public void ApplicationServicesAvailableFromTestServer() + { + var testService = new TestService(); + var builder = new WebHostBuilder() + .Configure(app => { }) + .ConfigureServices(services => + { + services.AddSingleton(testService); + }); + var server = new TestServer(builder); + + Assert.Equal(testService, server.Host.Services.GetRequiredService<TestService>()); + } + + [Fact] + public async Task RequestServicesAutoCreated() + { + var builder = new WebHostBuilder().Configure(app => + { + app.Run(context => + { + return context.Response.WriteAsync("RequestServices:" + (context.RequestServices != null)); + }); + }); + var server = new TestServer(builder); + + string result = await server.CreateClient().GetStringAsync("/path"); + Assert.Equal("RequestServices:True", result); + } + + public class CustomContainerStartup + { + public IServiceProvider Services; + public IServiceProvider ConfigureServices(IServiceCollection services) + { + Services = services.BuildServiceProvider(); + return Services; + } + + public void Configure(IApplicationBuilder app) + { + var applicationServices = app.ApplicationServices; + app.Run(async context => + { + await context.Response.WriteAsync("ApplicationServicesEqual:" + (applicationServices == Services)); + }); + } + + } + + [Fact] + public async Task CustomServiceProviderSetsApplicationServices() + { + var builder = new WebHostBuilder().UseStartup<CustomContainerStartup>(); + var server = new TestServer(builder); + string result = await server.CreateClient().GetStringAsync("/path"); + Assert.Equal("ApplicationServicesEqual:True", result); + } + + [Fact] + public void TestServerConstructorWithFeatureCollectionAllowsInitializingServerFeatures() + { + // Arrange + var url = "http://localhost:8000/appName/serviceName"; + var builder = new WebHostBuilder() + .UseUrls(url) + .Configure(applicationBuilder => + { + var serverAddressesFeature = applicationBuilder.ServerFeatures.Get<IServerAddressesFeature>(); + Assert.Contains(serverAddressesFeature.Addresses, s => string.Equals(s, url, StringComparison.Ordinal)); + }); + + + var featureCollection = new FeatureCollection(); + featureCollection.Set<IServerAddressesFeature>(new ServerAddressesFeature()); + + // Act + new TestServer(builder, featureCollection); + + // Assert + // Is inside configure callback + } + + [Fact] + public void TestServerConstructorWithNullFeatureCollectionThrows() + { + var builder = new WebHostBuilder() + .Configure(b => { }); + + Assert.Throws<ArgumentNullException>(() => new TestServer(builder, null)); + } + + public class TestService { public string Message { get; set; } } + + public class TestRequestServiceMiddleware + { + private RequestDelegate _next; + + public TestRequestServiceMiddleware(RequestDelegate next) + { + _next = next; + } + + public Task Invoke(HttpContext httpContext) + { + var services = new ServiceCollection(); + services.AddTransient<TestService>(); + httpContext.RequestServices = services.BuildServiceProvider(); + + return _next.Invoke(httpContext); + } + } + + public class RequestServicesFilter : IStartupFilter + { + public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) + { + return builder => + { + builder.UseMiddleware<TestRequestServiceMiddleware>(); + next(builder); + }; + } + } + + [Fact] + public async Task ExistingRequestServicesWillNotBeReplaced() + { + var builder = new WebHostBuilder().Configure(app => + { + app.Run(context => + { + var service = context.RequestServices.GetService<TestService>(); + return context.Response.WriteAsync("Found:" + (service != null)); + }); + }) + .ConfigureServices(services => + { + services.AddTransient<IStartupFilter, RequestServicesFilter>(); + }); + var server = new TestServer(builder); + + string result = await server.CreateClient().GetStringAsync("/path"); + Assert.Equal("Found:True", result); + } + + [Fact] + public async Task CanSetCustomServiceProvider() + { + var builder = new WebHostBuilder().Configure(app => + { + app.Run(context => + { + context.RequestServices = new ServiceCollection() + .AddTransient<TestService>() + .BuildServiceProvider(); + + var s = context.RequestServices.GetRequiredService<TestService>(); + + return context.Response.WriteAsync("Success"); + }); + }); + var server = new TestServer(builder); + + string result = await server.CreateClient().GetStringAsync("/path"); + Assert.Equal("Success", result); + } + + public class ReplaceServiceProvidersFeatureFilter : IStartupFilter, IServiceProvidersFeature + { + public ReplaceServiceProvidersFeatureFilter(IServiceProvider appServices, IServiceProvider requestServices) + { + ApplicationServices = appServices; + RequestServices = requestServices; + } + + public IServiceProvider ApplicationServices { get; set; } + + public IServiceProvider RequestServices { get; set; } + + public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) + { + return app => + { + app.Use(async (context, nxt) => + { + context.Features.Set<IServiceProvidersFeature>(this); + await nxt(); + }); + next(app); + }; + } + } + + [Fact] + public async Task ExistingServiceProviderFeatureWillNotBeReplaced() + { + var appServices = new ServiceCollection().BuildServiceProvider(); + var builder = new WebHostBuilder().Configure(app => + { + app.Run(context => + { + Assert.Equal(appServices, context.RequestServices); + return context.Response.WriteAsync("Success"); + }); + }) + .ConfigureServices(services => + { + services.AddSingleton<IStartupFilter>(new ReplaceServiceProvidersFeatureFilter(appServices, appServices)); + }); + var server = new TestServer(builder); + + var result = await server.CreateClient().GetStringAsync("/path"); + Assert.Equal("Success", result); + } + + public class NullServiceProvidersFeatureFilter : IStartupFilter, IServiceProvidersFeature + { + public IServiceProvider ApplicationServices { get; set; } + + public IServiceProvider RequestServices { get; set; } + + public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) + { + return app => + { + app.Use(async (context, nxt) => + { + context.Features.Set<IServiceProvidersFeature>(this); + await nxt(); + }); + next(app); + }; + } + } + + [Fact] + public async Task WillReplaceServiceProviderFeatureWithNullRequestServices() + { + var builder = new WebHostBuilder().Configure(app => + { + app.Run(context => + { + Assert.Null(context.RequestServices); + return context.Response.WriteAsync("Success"); + }); + }) + .ConfigureServices(services => + { + services.AddTransient<IStartupFilter, NullServiceProvidersFeatureFilter>(); + }); + var server = new TestServer(builder); + + var result = await server.CreateClient().GetStringAsync("/path"); + Assert.Equal("Success", result); + } + + [Fact] + public async Task CanAccessLogger() + { + var builder = new WebHostBuilder().Configure(app => + { + app.Run(context => + { + var logger = app.ApplicationServices.GetRequiredService<ILogger<HttpContext>>(); + return context.Response.WriteAsync("FoundLogger:" + (logger != null)); + }); + }); + var server = new TestServer(builder); + + string result = await server.CreateClient().GetStringAsync("/path"); + Assert.Equal("FoundLogger:True", result); + } + + [Fact] + public async Task CanAccessHttpContext() + { + var builder = new WebHostBuilder().Configure(app => + { + app.Run(context => + { + var accessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>(); + return context.Response.WriteAsync("HasContext:" + (accessor.HttpContext != null)); + }); + }) + .ConfigureServices(services => + { + services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); + }); + var server = new TestServer(builder); + + string result = await server.CreateClient().GetStringAsync("/path"); + Assert.Equal("HasContext:True", result); + } + + public class ContextHolder + { + public ContextHolder(IHttpContextAccessor accessor) + { + Accessor = accessor; + } + + public IHttpContextAccessor Accessor { get; set; } + } + + [Fact] + public async Task CanAddNewHostServices() + { + var builder = new WebHostBuilder().Configure(app => + { + app.Run(context => + { + var accessor = app.ApplicationServices.GetRequiredService<ContextHolder>(); + return context.Response.WriteAsync("HasContext:" + (accessor.Accessor.HttpContext != null)); + }); + }) + .ConfigureServices(services => + { + services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); + services.AddSingleton<ContextHolder>(); + }); + var server = new TestServer(builder); + + string result = await server.CreateClient().GetStringAsync("/path"); + Assert.Equal("HasContext:True", result); + } + + [Fact] + public async Task CreateInvokesApp() + { + var builder = new WebHostBuilder().Configure(app => + { + app.Run(context => + { + return context.Response.WriteAsync("CreateInvokesApp"); + }); + }); + var server = new TestServer(builder); + + string result = await server.CreateClient().GetStringAsync("/path"); + Assert.Equal("CreateInvokesApp", result); + } + + [Fact] + public async Task DisposeStreamIgnored() + { + var builder = new WebHostBuilder().Configure(app => + { + app.Run(async context => + { + await context.Response.WriteAsync("Response"); + context.Response.Body.Dispose(); + }); + }); + var server = new TestServer(builder); + + HttpResponseMessage result = await server.CreateClient().GetAsync("/"); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal("Response", await result.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task DisposedServerThrows() + { + var builder = new WebHostBuilder().Configure(app => + { + app.Run(async context => + { + await context.Response.WriteAsync("Response"); + context.Response.Body.Dispose(); + }); + }); + var server = new TestServer(builder); + + HttpResponseMessage result = await server.CreateClient().GetAsync("/"); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + server.Dispose(); + await Assert.ThrowsAsync<ObjectDisposedException>(() => server.CreateClient().GetAsync("/")); + } + + [Fact] + public async Task CancelAborts() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.Run(context => + { + TaskCompletionSource<int> tcs = new TaskCompletionSource<int>(); + tcs.SetCanceled(); + return tcs.Task; + }); + }); + var server = new TestServer(builder); + + await Assert.ThrowsAsync<TaskCanceledException>(async () => { string result = await server.CreateClient().GetStringAsync("/path"); }); + } + + [Fact] + public async Task CanCreateViaStartupType() + { + var builder = new WebHostBuilder() + .UseStartup<TestStartup>(); + var server = new TestServer(builder); + HttpResponseMessage result = await server.CreateClient().GetAsync("/"); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal("FoundService:True", await result.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task CanCreateViaStartupTypeAndSpecifyEnv() + { + var builder = new WebHostBuilder() + .UseStartup<TestStartup>() + .UseEnvironment("Foo"); + var server = new TestServer(builder); + + HttpResponseMessage result = await server.CreateClient().GetAsync("/"); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal("FoundFoo:False", await result.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task BeginEndDiagnosticAvailable() + { + DiagnosticListener diagnosticListener = null; + + var builder = new WebHostBuilder() + .Configure(app => + { + diagnosticListener = app.ApplicationServices.GetRequiredService<DiagnosticListener>(); + app.Run(context => + { + return context.Response.WriteAsync("Hello World"); + }); + }); + var server = new TestServer(builder); + + var listener = new TestDiagnosticListener(); + diagnosticListener.SubscribeWithAdapter(listener); + var result = await server.CreateClient().GetStringAsync("/path"); + + // This ensures that all diagnostics are completely written to the diagnostic listener + Thread.Sleep(1000); + + Assert.Equal("Hello World", result); + Assert.NotNull(listener.BeginRequest?.HttpContext); + Assert.NotNull(listener.EndRequest?.HttpContext); + Assert.Null(listener.UnhandledException); + } + + [Fact] + public async Task ExceptionDiagnosticAvailable() + { + DiagnosticListener diagnosticListener = null; + var builder = new WebHostBuilder().Configure(app => + { + diagnosticListener = app.ApplicationServices.GetRequiredService<DiagnosticListener>(); + app.Run(context => + { + throw new Exception("Test exception"); + }); + }); + var server = new TestServer(builder); + + var listener = new TestDiagnosticListener(); + diagnosticListener.SubscribeWithAdapter(listener); + await Assert.ThrowsAsync<Exception>(() => server.CreateClient().GetAsync("/path")); + + // This ensures that all diagnostics are completely written to the diagnostic listener + Thread.Sleep(1000); + + Assert.NotNull(listener.BeginRequest?.HttpContext); + Assert.Null(listener.EndRequest?.HttpContext); + Assert.NotNull(listener.UnhandledException?.HttpContext); + Assert.NotNull(listener.UnhandledException?.Exception); + } + + public class TestDiagnosticListener + { + public class OnBeginRequestEventData + { + public IProxyHttpContext HttpContext { get; set; } + } + + public OnBeginRequestEventData BeginRequest { get; set; } + + [DiagnosticName("Microsoft.AspNetCore.Hosting.BeginRequest")] + public virtual void OnBeginRequest(IProxyHttpContext httpContext) + { + BeginRequest = new OnBeginRequestEventData() + { + HttpContext = httpContext, + }; + } + + public class OnEndRequestEventData + { + public IProxyHttpContext HttpContext { get; set; } + } + + public OnEndRequestEventData EndRequest { get; set; } + + [DiagnosticName("Microsoft.AspNetCore.Hosting.EndRequest")] + public virtual void OnEndRequest(IProxyHttpContext httpContext) + { + EndRequest = new OnEndRequestEventData() + { + HttpContext = httpContext, + }; + } + + public class OnUnhandledExceptionEventData + { + public IProxyHttpContext HttpContext { get; set; } + public IProxyException Exception { get; set; } + } + + public OnUnhandledExceptionEventData UnhandledException { get; set; } + + [DiagnosticName("Microsoft.AspNetCore.Hosting.UnhandledException")] + public virtual void OnUnhandledException(IProxyHttpContext httpContext, IProxyException exception) + { + UnhandledException = new OnUnhandledExceptionEventData() + { + HttpContext = httpContext, + Exception = exception, + }; + } + } + + public interface IProxyHttpContext + { + } + + public interface IProxyException + { + } + + public class Startup + { + public void Configure(IApplicationBuilder builder) + { + builder.Run(ctx => ctx.Response.WriteAsync("Startup")); + } + } + + public class SimpleService + { + public SimpleService() + { + } + + public string Message { get; set; } + } + + public class TestStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton<SimpleService>(); + } + + public void ConfigureFooServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app) + { + app.Run(context => + { + var service = app.ApplicationServices.GetRequiredService<SimpleService>(); + return context.Response.WriteAsync("FoundService:" + (service != null)); + }); + } + + public void ConfigureFoo(IApplicationBuilder app) + { + app.Run(context => + { + var service = app.ApplicationServices.GetService<SimpleService>(); + return context.Response.WriteAsync("FoundFoo:" + (service != null)); + }); + } + } + } +} diff --git a/src/Hosting/WindowsServices/src/Microsoft.AspNetCore.Hosting.WindowsServices.csproj b/src/Hosting/WindowsServices/src/Microsoft.AspNetCore.Hosting.WindowsServices.csproj new file mode 100644 index 0000000000000000000000000000000000000000..0c4112a61c28b7ae0ff4599d6e9dbcae85c694ec --- /dev/null +++ b/src/Hosting/WindowsServices/src/Microsoft.AspNetCore.Hosting.WindowsServices.csproj @@ -0,0 +1,21 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>ASP.NET Core hosting infrastructure and startup logic for web applications running within a Windows service.</Description> + <TargetFrameworks>net461;netstandard2.0</TargetFrameworks> + <NoWarn>$(NoWarn);CS1591</NoWarn> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore;hosting</PackageTags> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Hosting" /> + </ItemGroup> + <ItemGroup Condition=" '$(TargetFramework)' == 'net461' "> + <Reference Include="System.ServiceProcess" /> + </ItemGroup> + <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' "> + <Reference Include="System.ServiceProcess.ServiceController" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/WindowsServices/src/WebHostService.cs b/src/Hosting/WindowsServices/src/WebHostService.cs new file mode 100644 index 0000000000000000000000000000000000000000..f468d05fe3eb36067b1f1b2d2519c8a0522f20ce --- /dev/null +++ b/src/Hosting/WindowsServices/src/WebHostService.cs @@ -0,0 +1,84 @@ +// 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.ServiceProcess; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.WindowsServices +{ + /// <summary> + /// Provides an implementation of a Windows service that hosts ASP.NET Core. + /// </summary> + public class WebHostService : ServiceBase + { + private IWebHost _host; + private bool _stopRequestedByWindows; + + /// <summary> + /// Creates an instance of <c>WebHostService</c> which hosts the specified web application. + /// </summary> + /// <param name="host">The configured web host containing the web application to host in the Windows service.</param> + public WebHostService(IWebHost host) + { + _host = host ?? throw new ArgumentNullException(nameof(host)); + } + + protected sealed override void OnStart(string[] args) + { + OnStarting(args); + + _host + .Services + .GetRequiredService<IApplicationLifetime>() + .ApplicationStopped + .Register(() => + { + if (!_stopRequestedByWindows) + { + Stop(); + } + }); + + _host.Start(); + + OnStarted(); + } + + protected sealed override void OnStop() + { + _stopRequestedByWindows = true; + OnStopping(); + try + { + _host.StopAsync().GetAwaiter().GetResult(); + } + finally + { + _host.Dispose(); + OnStopped(); + } + } + + /// <summary> + /// Executes before ASP.NET Core starts. + /// </summary> + /// <param name="args">The command line arguments passed to the service.</param> + protected virtual void OnStarting(string[] args) { } + + /// <summary> + /// Executes after ASP.NET Core starts. + /// </summary> + protected virtual void OnStarted() { } + + /// <summary> + /// Executes before ASP.NET Core shuts down. + /// </summary> + protected virtual void OnStopping() { } + + /// <summary> + /// Executes after ASP.NET Core shuts down. + /// </summary> + protected virtual void OnStopped() { } + } +} diff --git a/src/Hosting/WindowsServices/src/WebHostWindowsServiceExtensions.cs b/src/Hosting/WindowsServices/src/WebHostWindowsServiceExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..9e657fbe3e3b48c439b908039f0771b9d94e6bba --- /dev/null +++ b/src/Hosting/WindowsServices/src/WebHostWindowsServiceExtensions.cs @@ -0,0 +1,42 @@ +// 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.ServiceProcess; + +namespace Microsoft.AspNetCore.Hosting.WindowsServices +{ + /// <summary> + /// Extensions to <see cref="IWebHost"/> for hosting inside a Windows service. + /// </summary> + public static class WebHostWindowsServiceExtensions + { + /// <summary> + /// Runs the specified web application inside a Windows service and blocks until the service is stopped. + /// </summary> + /// <param name="host">An instance of the <see cref="IWebHost"/> to host in the Windows service.</param> + /// <example> + /// This example shows how to use <see cref="RunAsService"/>. + /// <code> + /// public class Program + /// { + /// public static void Main(string[] args) + /// { + /// var config = WebHostConfiguration.GetDefault(args); + /// + /// var host = new WebHostBuilder() + /// .UseConfiguration(config) + /// .Build(); + /// + /// // This call will block until the service is stopped. + /// host.RunAsService(); + /// } + /// } + /// </code> + /// </example> + public static void RunAsService(this IWebHost host) + { + var webHostService = new WebHostService(host); + ServiceBase.Run(webHostService); + } + } +} diff --git a/src/Hosting/WindowsServices/src/baseline.netcore.json b/src/Hosting/WindowsServices/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..a37e8c99eff14a0d438f66c0709825a2c3d6c3c2 --- /dev/null +++ b/src/Hosting/WindowsServices/src/baseline.netcore.json @@ -0,0 +1,122 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Hosting.WindowsServices, Version=2.1.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Hosting.WindowsServices.WebHostService", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.ServiceProcess.ServiceBase", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "OnStart", + "Parameters": [ + { + "Name": "args", + "Type": "System.String[]" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStop", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStarting", + "Parameters": [ + { + "Name": "args", + "Type": "System.String[]" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStarted", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStopping", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStopped", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "host", + "Type": "Microsoft.AspNetCore.Hosting.IWebHost" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.WindowsServices.WebHostWindowsServiceExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "RunAsService", + "Parameters": [ + { + "Name": "host", + "Type": "Microsoft.AspNetCore.Hosting.IWebHost" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Hosting/WindowsServices/src/baseline.netframework.json b/src/Hosting/WindowsServices/src/baseline.netframework.json new file mode 100644 index 0000000000000000000000000000000000000000..9850e83f45539c8dd8cc6d40ecb50b6d8e51b78a --- /dev/null +++ b/src/Hosting/WindowsServices/src/baseline.netframework.json @@ -0,0 +1,122 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Hosting.WindowsServices, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Hosting.WindowsServices.WebHostService", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.ServiceProcess.ServiceBase", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "OnStart", + "Parameters": [ + { + "Name": "args", + "Type": "System.String[]" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStop", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStarting", + "Parameters": [ + { + "Name": "args", + "Type": "System.String[]" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStarted", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStopping", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStopped", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "host", + "Type": "Microsoft.AspNetCore.Hosting.IWebHost" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Hosting.WindowsServices.WebHostWindowsServiceExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "RunAsService", + "Parameters": [ + { + "Name": "host", + "Type": "Microsoft.AspNetCore.Hosting.IWebHost" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Hosting/samples/GenericWebHost/FakeServer.cs b/src/Hosting/samples/GenericWebHost/FakeServer.cs new file mode 100644 index 0000000000000000000000000000000000000000..bb1d66984680793b2c88c2975d22874fd381d617 --- /dev/null +++ b/src/Hosting/samples/GenericWebHost/FakeServer.cs @@ -0,0 +1,32 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace GenericWebHost +{ + // We can't reference real servers in this sample without creating a circular repo dependency. + // This fake server lets us at least run the code. + public class FakeServer : IServer + { + public IFeatureCollection Features => new FeatureCollection(); + + public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public void Dispose() + { + } + } + + public static class FakeServerWebHostBuilderExtensions + { + public static IHostBuilder UseFakeServer(this IHostBuilder builder) + { + return builder.ConfigureServices((builderContext, services) => services.AddSingleton<IServer, FakeServer>()); + } + } +} diff --git a/src/Hosting/samples/GenericWebHost/GenericWebHost.csproj b/src/Hosting/samples/GenericWebHost/GenericWebHost.csproj new file mode 100644 index 0000000000000000000000000000000000000000..74f18791c84cc1ed4cb73408a1896c729af63594 --- /dev/null +++ b/src/Hosting/samples/GenericWebHost/GenericWebHost.csproj @@ -0,0 +1,18 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks> + <LangVersion>latest</LangVersion> + <SignAssembly>true</SignAssembly> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Hosting" /> + <Reference Include="Microsoft.Extensions.Hosting" /> + <Reference Include="Microsoft.Extensions.Configuration.CommandLine" /> + <Reference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" /> + <Reference Include="Microsoft.Extensions.Configuration.Json" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/samples/GenericWebHost/Program.cs b/src/Hosting/samples/GenericWebHost/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..4879031f56097ef75cb66e8e6f031d7fe69d58f2 --- /dev/null +++ b/src/Hosting/samples/GenericWebHost/Program.cs @@ -0,0 +1,41 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace GenericWebHost +{ + public class Program + { + public static async Task Main(string[] args) + { + var host = new HostBuilder() + .ConfigureAppConfiguration((hostContext, config) => + { + config.AddEnvironmentVariables(); + config.AddJsonFile("appsettings.json", optional: true); + config.AddCommandLine(args); + }) + .ConfigureServices((hostContext, services) => + { + }) + .UseFakeServer() + .ConfigureWebHost((hostContext, app) => + { + app.Run(async (context) => + { + await context.Response.WriteAsync("Hello World!"); + }); + }) + .UseConsoleLifetime() + .Build(); + + var s = host.Services; + + await host.RunAsync(); + } + } +} diff --git a/src/Hosting/samples/GenericWebHost/WebHostExtensions.cs b/src/Hosting/samples/GenericWebHost/WebHostExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..bf5567d81ae68d04178a61f0266ce5e6dd35e779 --- /dev/null +++ b/src/Hosting/samples/GenericWebHost/WebHostExtensions.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ObjectPool; + +namespace GenericWebHost +{ + public static class WebHostExtensions + { + public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<HostBuilderContext, IApplicationBuilder> configureApp) + { + return builder.ConfigureServices((bulderContext, services) => + { + services.Configure<WebHostServiceOptions>(options => + { + options.ConfigureApp = configureApp; + }); + services.AddHostedService<WebHostService>(); + + var listener = new DiagnosticListener("Microsoft.AspNetCore"); + services.AddSingleton<DiagnosticListener>(listener); + services.AddSingleton<DiagnosticSource>(listener); + + services.AddTransient<IHttpContextFactory, HttpContextFactory>(); + services.AddScoped<IMiddlewareFactory, MiddlewareFactory>(); + + // Conjure up a RequestServices + services.AddTransient<IStartupFilter, AutoRequestServicesStartupFilter>(); + services.AddTransient<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>(); + + // Ensure object pooling is available everywhere. + services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>(); + }); + } + } +} diff --git a/src/Hosting/samples/GenericWebHost/WebHostService.cs b/src/Hosting/samples/GenericWebHost/WebHostService.cs new file mode 100644 index 0000000000000000000000000000000000000000..1ac316178febf8d025419582b9cd1aae5034178d --- /dev/null +++ b/src/Hosting/samples/GenericWebHost/WebHostService.cs @@ -0,0 +1,62 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace GenericWebHost +{ + internal class WebHostService : IHostedService + { + public WebHostService(IOptions<WebHostServiceOptions> options, IServiceProvider services, HostBuilderContext hostBuilderContext, IServer server, + ILogger<WebHostService> logger, DiagnosticListener diagnosticListener, IHttpContextFactory httpContextFactory) + { + Options = options?.Value ?? throw new System.ArgumentNullException(nameof(options)); + + if (Options.ConfigureApp == null) + { + throw new ArgumentException(nameof(Options.ConfigureApp)); + } + + Services = services ?? throw new ArgumentNullException(nameof(services)); + HostBuilderContext = hostBuilderContext ?? throw new ArgumentNullException(nameof(hostBuilderContext)); + Server = server ?? throw new ArgumentNullException(nameof(server)); + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + DiagnosticListener = diagnosticListener ?? throw new ArgumentNullException(nameof(diagnosticListener)); + HttpContextFactory = httpContextFactory ?? throw new ArgumentNullException(nameof(httpContextFactory)); + } + + public WebHostServiceOptions Options { get; } + public IServiceProvider Services { get; } + public HostBuilderContext HostBuilderContext { get; } + public IServer Server { get; } + public ILogger<WebHostService> Logger { get; } + public DiagnosticListener DiagnosticListener { get; } + public IHttpContextFactory HttpContextFactory { get; } + + public Task StartAsync(CancellationToken cancellationToken) + { + Server.Features.Get<IServerAddressesFeature>()?.Addresses.Add("http://localhost:5000"); + + var builder = new ApplicationBuilder(Services, Server.Features); + Options.ConfigureApp(HostBuilderContext, builder); + var app = builder.Build(); + + var httpApp = new HostingApplication(app, Logger, DiagnosticListener, HttpContextFactory); + return Server.StartAsync(httpApp, cancellationToken); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Server.StopAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Hosting/samples/GenericWebHost/WebHostServiceOptions.cs b/src/Hosting/samples/GenericWebHost/WebHostServiceOptions.cs new file mode 100644 index 0000000000000000000000000000000000000000..123dcf87907726c092db41630ee3bf5aa6bf9b01 --- /dev/null +++ b/src/Hosting/samples/GenericWebHost/WebHostServiceOptions.cs @@ -0,0 +1,11 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Hosting; + +namespace GenericWebHost +{ + public class WebHostServiceOptions + { + public Action<HostBuilderContext, IApplicationBuilder> ConfigureApp { get; internal set; } + } +} \ No newline at end of file diff --git a/src/Hosting/samples/SampleStartups/FakeServer.cs b/src/Hosting/samples/SampleStartups/FakeServer.cs new file mode 100644 index 0000000000000000000000000000000000000000..ebdfdb383eb7fa9ad90cfd41e12d257afc6cbb03 --- /dev/null +++ b/src/Hosting/samples/SampleStartups/FakeServer.cs @@ -0,0 +1,32 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; + +namespace SampleStartups +{ + // We can't reference real servers in this sample without creating a circular repo dependency. + // This fake server lets us at least run the code. + public class FakeServer : IServer + { + public IFeatureCollection Features => new FeatureCollection(); + + public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public void Dispose() + { + } + } + + public static class FakeServerWebHostBuilderExtensions + { + public static IWebHostBuilder UseFakeServer(this IWebHostBuilder builder) + { + return builder.ConfigureServices(services => services.AddSingleton<IServer, FakeServer>()); + } + } +} diff --git a/src/Hosting/samples/SampleStartups/SampleStartups.csproj b/src/Hosting/samples/SampleStartups/SampleStartups.csproj new file mode 100644 index 0000000000000000000000000000000000000000..9c881e56e36e18e505154747e47949d127995ba1 --- /dev/null +++ b/src/Hosting/samples/SampleStartups/SampleStartups.csproj @@ -0,0 +1,15 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks> + <StartupObject>SampleStartups.StartupInjection</StartupObject> + <OutputType>exe</OutputType> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Hosting" /> + <Reference Include="Microsoft.Extensions.Configuration.CommandLine" /> + <Reference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" /> + <Reference Include="Microsoft.Extensions.Configuration.Json" /> + </ItemGroup> +</Project> diff --git a/src/Hosting/samples/SampleStartups/StartupBlockingOnStart.cs b/src/Hosting/samples/SampleStartups/StartupBlockingOnStart.cs new file mode 100644 index 0000000000000000000000000000000000000000..65a226f3cdee2ce045c6cc46c3bee6b185bb4954 --- /dev/null +++ b/src/Hosting/samples/SampleStartups/StartupBlockingOnStart.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +// Note that this sample will not run. It is only here to illustrate usage patterns. + +namespace SampleStartups +{ + public class StartupBlockingOnStart : StartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public override void Configure(IApplicationBuilder app) + { + app.Run(async (context) => + { + await context.Response.WriteAsync("Hello World!"); + }); + } + + // Entry point for the application. + public static void Main(string[] args) + { + var config = new ConfigurationBuilder().AddCommandLine(args).Build(); + + var host = new WebHostBuilder() + .UseConfiguration(config) + .UseFakeServer() + .UseStartup<StartupBlockingOnStart>() + .Build(); + + using (host) + { + host.Start(); + Console.ReadLine(); + } + } + } +} diff --git a/src/Hosting/samples/SampleStartups/StartupConfigureAddresses.cs b/src/Hosting/samples/SampleStartups/StartupConfigureAddresses.cs new file mode 100644 index 0000000000000000000000000000000000000000..8413c47c90ed59e6395e477beefe694830c342b7 --- /dev/null +++ b/src/Hosting/samples/SampleStartups/StartupConfigureAddresses.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; + +// Note that this sample will not run. It is only here to illustrate usage patterns. + +namespace SampleStartups +{ + public class StartupConfigureAddresses : StartupBase + { + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public override void Configure(IApplicationBuilder app) + { + app.Run(async (context) => + { + await context.Response.WriteAsync("Hello World!"); + }); + } + + // Entry point for the application. + public static void Main(string[] args) + { + var config = new ConfigurationBuilder().AddCommandLine(args).Build(); + + var host = new WebHostBuilder() + .UseConfiguration(config) + .UseFakeServer() + .UseStartup<StartupConfigureAddresses>() + .UseUrls("http://localhost:5000", "http://localhost:5001") + .Build(); + + host.Run(); + } + } +} diff --git a/src/Hosting/samples/SampleStartups/StartupExternallyControlled.cs b/src/Hosting/samples/SampleStartups/StartupExternallyControlled.cs new file mode 100644 index 0000000000000000000000000000000000000000..68ec11c9b06332727fd68b7b1163d3bfe218dfe8 --- /dev/null +++ b/src/Hosting/samples/SampleStartups/StartupExternallyControlled.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; + +// Note that this sample will not run. It is only here to illustrate usage patterns. + +namespace SampleStartups +{ + public class StartupExternallyControlled : StartupBase + { + private IWebHost _host; + private readonly List<string> _urls = new List<string>(); + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public override void Configure(IApplicationBuilder app) + { + app.Run(async (context) => + { + await context.Response.WriteAsync("Hello World!"); + }); + } + + public StartupExternallyControlled() + { + } + + public void Start() + { + _host = new WebHostBuilder() + //.UseKestrel() + .UseFakeServer() + .UseStartup<StartupExternallyControlled>() + .Start(_urls.ToArray()); + } + + public async Task StopAsync() + { + await _host.StopAsync(TimeSpan.FromSeconds(5)); + _host.Dispose(); + } + + public void AddUrl(string url) + { + _urls.Add(url); + } + } +} diff --git a/src/Hosting/samples/SampleStartups/StartupFullControl.cs b/src/Hosting/samples/SampleStartups/StartupFullControl.cs new file mode 100644 index 0000000000000000000000000000000000000000..272e747446476793e3cadd20cd5723e3adb507fd --- /dev/null +++ b/src/Hosting/samples/SampleStartups/StartupFullControl.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +// Note that this sample will not run. It is only here to illustrate usage patterns. + +namespace SampleStartups +{ + public class StartupFullControl + { + public static void Main(string[] args) + { + var config = new ConfigurationBuilder() + .AddCommandLine(args) + .AddEnvironmentVariables(prefix: "ASPNETCORE_") + .AddJsonFile("hosting.json", optional: true) + .Build(); + + var host = new WebHostBuilder() + .UseConfiguration(config) // Default set of configurations to use, may be subsequently overridden + //.UseKestrel() + .UseFakeServer() + .UseContentRoot(Directory.GetCurrentDirectory()) // Override the content root with the current directory + .UseUrls("http://*:1000", "https://*:902") + .UseEnvironment(EnvironmentName.Development) + .UseWebRoot("public") + .ConfigureServices(services => + { + // Configure services that the application can see + services.AddSingleton<IMyCustomService, MyCustomService>(); + }) + .Configure(app => + { + // Write the application inline, this won't call any startup class in the assembly + + app.Use(next => context => + { + return next(context); + }); + }) + .Build(); + + host.Run(); + } + } + + public class MyHostLoggerProvider : ILoggerProvider + { + public ILogger CreateLogger(string categoryName) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + throw new NotImplementedException(); + } + } + + public interface IMyCustomService + { + void Go(); + } + + public class MyCustomService : IMyCustomService + { + public void Go() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Hosting/samples/SampleStartups/StartupHelloWorld.cs b/src/Hosting/samples/SampleStartups/StartupHelloWorld.cs new file mode 100644 index 0000000000000000000000000000000000000000..88a77cfc6c4789c24d2579c978b1da33f7acb9fd --- /dev/null +++ b/src/Hosting/samples/SampleStartups/StartupHelloWorld.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; + +// Note that this sample will not run. It is only here to illustrate usage patterns. + +namespace SampleStartups +{ + public class StartupHelloWorld : StartupBase + { + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public override void Configure(IApplicationBuilder app) + { + app.Run(async (context) => + { + await context.Response.WriteAsync("Hello World!"); + }); + } + + // Entry point for the application. + public static void Main(string[] args) + { + var host = new WebHostBuilder() + //.UseKestrel() + .UseFakeServer() + .UseStartup<StartupHelloWorld>() + .Build(); + + host.Run(); + } + } +} diff --git a/src/Hosting/samples/SampleStartups/StartupInjection.cs b/src/Hosting/samples/SampleStartups/StartupInjection.cs new file mode 100644 index 0000000000000000000000000000000000000000..381d621a10f0453cc8c85823d43c8ff104ed40c6 --- /dev/null +++ b/src/Hosting/samples/SampleStartups/StartupInjection.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +// HostingStartup's in the primary assembly are run automatically. +[assembly: HostingStartup(typeof(SampleStartups.StartupInjection))] + +namespace SampleStartups +{ + public class StartupInjection : IHostingStartup + { + public void Configure(IWebHostBuilder builder) + { + builder.UseStartup<InjectedStartup>(); + } + + // Entry point for the application. + public static void Main(string[] args) + { + var host = new WebHostBuilder() + //.UseKestrel() + .UseFakeServer() + // Each of these three sets ApplicationName to the current assembly, which is needed in order to + // scan the assembly for HostingStartupAttributes. + // .UseSetting(WebHostDefaults.ApplicationKey, "SampleStartups") + // .Configure(_ => { }) + .UseStartup<NormalStartup>() + .Build(); + + host.Run(); + } + } + + public class NormalStartup + { + public void ConfigureServices(IServiceCollection services) + { + Console.WriteLine("NormalStartup.ConfigureServices"); + } + + public void Configure(IApplicationBuilder app) + { + Console.WriteLine("NormalStartup.Configure"); + app.Run(async (context) => + { + await context.Response.WriteAsync("Hello World!"); + }); + } + } + + public class InjectedStartup + { + public void ConfigureServices(IServiceCollection services) + { + Console.WriteLine("InjectedStartup.ConfigureServices"); + } + + public void Configure(IApplicationBuilder app) + { + Console.WriteLine("InjectedStartup.Configure"); + app.Run(async (context) => + { + await context.Response.WriteAsync("Hello World!"); + }); + } + } +} diff --git a/src/Hosting/test/FunctionalTests/Microsoft.AspNetCore.Hosting.FunctionalTests.csproj b/src/Hosting/test/FunctionalTests/Microsoft.AspNetCore.Hosting.FunctionalTests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..3daae1c597519cd742895d86f0679673d483b363 --- /dev/null +++ b/src/Hosting/test/FunctionalTests/Microsoft.AspNetCore.Hosting.FunctionalTests.csproj @@ -0,0 +1,17 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>netcoreapp2.0</TargetFrameworks> + </PropertyGroup> + + <ItemGroup> + <Content Include="testroot\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" /> + </ItemGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Server.IntegrationTesting" /> + <Reference Include="Microsoft.AspNetCore.Hosting" /> + <Reference Include="Microsoft.Extensions.Logging.Console" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/test/FunctionalTests/Properties/AssemblyInfo.cs b/src/Hosting/test/FunctionalTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..a82d7bc1e5c5d3b634c6df188132a8cc436fa594 --- /dev/null +++ b/src/Hosting/test/FunctionalTests/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] diff --git a/src/Hosting/test/FunctionalTests/ShutdownTests.cs b/src/Hosting/test/FunctionalTests/ShutdownTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..1c229e96b88f3517322835db31b8998a2a7e0212 --- /dev/null +++ b/src/Hosting/test/FunctionalTests/ShutdownTests.cs @@ -0,0 +1,144 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Hosting.FunctionalTests +{ + public class ShutdownTests : LoggedTest + { + private static readonly string StartedMessage = "Started"; + private static readonly string CompletionMessage = "Stopping firing\n" + + "Stopping end\n" + + "Stopped firing\n" + + "Stopped end"; + + public ShutdownTests(ITestOutputHelper output) : base(output) { } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task ShutdownTestRun() + { + await ExecuteShutdownTest(nameof(ShutdownTestRun), "Run"); + } + + [ConditionalFact(Skip = "https://github.com/aspnet/Hosting/issues/1214")] + [OSSkipCondition(OperatingSystems.Windows)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task ShutdownTestWaitForShutdown() + { + await ExecuteShutdownTest(nameof(ShutdownTestWaitForShutdown), "WaitForShutdown"); + } + + private async Task ExecuteShutdownTest(string testName, string shutdownMechanic) + { + using (StartLog(out var loggerFactory)) + { + var logger = loggerFactory.CreateLogger(testName); + + var applicationPath = Path.Combine(TestPathUtilities.GetSolutionRootDirectory("Hosting"), "test", "TestAssets", + "Microsoft.AspNetCore.Hosting.TestSites"); + + var deploymentParameters = new DeploymentParameters( + applicationPath, + ServerType.Kestrel, + RuntimeFlavor.CoreClr, + RuntimeArchitecture.x64) + { + EnvironmentName = "Shutdown", + TargetFramework = "netcoreapp2.0", + ApplicationType = ApplicationType.Portable, + PublishApplicationBeforeDeployment = true, + StatusMessagesEnabled = false + }; + + deploymentParameters.EnvironmentVariables["ASPNETCORE_STARTMECHANIC"] = shutdownMechanic; + + using (var deployer = new SelfHostDeployer(deploymentParameters, loggerFactory)) + { + await deployer.DeployAsync(); + + var started = new ManualResetEventSlim(); + var completed = new ManualResetEventSlim(); + var output = string.Empty; + deployer.HostProcess.OutputDataReceived += (sender, args) => + { + if (!string.IsNullOrEmpty(args.Data) && args.Data.StartsWith(StartedMessage)) + { + started.Set(); + output += args.Data.Substring(StartedMessage.Length) + '\n'; + } + else + { + output += args.Data + '\n'; + } + + if (output.Contains(CompletionMessage)) + { + completed.Set(); + } + }; + + started.Wait(50000); + + if (!started.IsSet) + { + throw new InvalidOperationException("Application did not start successfully"); + } + + SendSIGINT(deployer.HostProcess.Id); + + WaitForExitOrKill(deployer.HostProcess); + + completed.Wait(50000); + + if (!started.IsSet) + { + throw new InvalidOperationException($"Application did not write the expected output. The received output is: {output}"); + } + + output = output.Trim('\n'); + + Assert.Equal(CompletionMessage, output); + } + } + } + + + private static void SendSIGINT(int processId) + { + var startInfo = new ProcessStartInfo + { + FileName = "kill", + Arguments = processId.ToString(), + RedirectStandardOutput = true, + UseShellExecute = false + }; + + var process = Process.Start(startInfo); + WaitForExitOrKill(process); + } + + private static void WaitForExitOrKill(Process process) + { + process.WaitForExit(1000); + if (!process.HasExited) + { + process.Kill(); + } + + Assert.Equal(0, process.ExitCode); + } + } +} diff --git a/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs b/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..4a56c432be92df015b097243dab9d7a3961f8594 --- /dev/null +++ b/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Hosting.FunctionalTests +{ + public class WebHostBuilderTests : LoggedTest + { + public WebHostBuilderTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task InjectedStartup_DefaultApplicationNameIsEntryAssembly_CoreClr() + => await InjectedStartup_DefaultApplicationNameIsEntryAssembly(RuntimeFlavor.CoreClr); + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.MacOSX)] + [OSSkipCondition(OperatingSystems.Linux)] + public async Task InjectedStartup_DefaultApplicationNameIsEntryAssembly_Clr() + => await InjectedStartup_DefaultApplicationNameIsEntryAssembly(RuntimeFlavor.Clr); + + private async Task InjectedStartup_DefaultApplicationNameIsEntryAssembly(RuntimeFlavor runtimeFlavor) + { + using (StartLog(out var loggerFactory)) + { + var logger = loggerFactory.CreateLogger(nameof(InjectedStartup_DefaultApplicationNameIsEntryAssembly)); + + var applicationPath = Path.Combine(TestPathUtilities.GetSolutionRootDirectory("Hosting"), "test", "TestAssets", "IStartupInjectionAssemblyName"); + + var deploymentParameters = new DeploymentParameters( + applicationPath, + ServerType.Kestrel, + runtimeFlavor, + RuntimeArchitecture.x64) + { + TargetFramework = runtimeFlavor == RuntimeFlavor.Clr ? "net461" : "netcoreapp2.0", + ApplicationType = ApplicationType.Portable, + StatusMessagesEnabled = false + }; + + using (var deployer = new SelfHostDeployer(deploymentParameters, loggerFactory)) + { + await deployer.DeployAsync(); + + string output = string.Empty; + var mre = new ManualResetEventSlim(); + deployer.HostProcess.OutputDataReceived += (sender, args) => + { + if (!string.IsNullOrWhiteSpace(args.Data)) + { + output += args.Data + '\n'; + mre.Set(); + } + }; + + mre.Wait(50000); + + output = output.Trim('\n'); + + Assert.Equal($"IStartupInjectionAssemblyName", output); + } + } + } + } +} diff --git a/src/Hosting/test/WebHostBuilderFactory.Tests/Microsoft.AspNetCore.Hosting.WebHostBuilderFactory.Tests.csproj b/src/Hosting/test/WebHostBuilderFactory.Tests/Microsoft.AspNetCore.Hosting.WebHostBuilderFactory.Tests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..95412eec3039b9da16db7f54a6c92322820c84ec --- /dev/null +++ b/src/Hosting/test/WebHostBuilderFactory.Tests/Microsoft.AspNetCore.Hosting.WebHostBuilderFactory.Tests.csproj @@ -0,0 +1,22 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks> + </PropertyGroup> + + <ItemGroup> + <Compile Include="$(SharedSourceRoot)Hosting.WebHostBuilderFactory\**\*.cs" /> + </ItemGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\testassets\BuildWebHostPatternTestSite\BuildWebHostPatternTestSite.csproj" /> + <ProjectReference Include="..\testassets\IStartupInjectionAssemblyName\IStartupInjectionAssemblyName.csproj" /> + <ProjectReference Include="..\testassets\CreateWebHostBuilderInvalidSignature\CreateWebHostBuilderInvalidSignature.csproj" /> + <ProjectReference Include="..\testassets\BuildWebHostInvalidSignature\BuildWebHostInvalidSignature.csproj" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/test/WebHostBuilderFactory.Tests/WebHostFactoryResolverTests.cs b/src/Hosting/test/WebHostBuilderFactory.Tests/WebHostFactoryResolverTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..f2732f75c48a88629fa824cd794bdfc7040a5aaf --- /dev/null +++ b/src/Hosting/test/WebHostBuilderFactory.Tests/WebHostFactoryResolverTests.cs @@ -0,0 +1,84 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Hosting.WebHostBuilderFactory.Tests +{ + public class WebHostFactoryResolverTests + { + [Fact] + public void CanFindWebHostBuilder_CreateWebHostBuilderPattern() + { + // Arrange & Act + var resolverResult = WebHostFactoryResolver.ResolveWebHostBuilderFactory<IWebHost, IWebHostBuilder>(typeof(IStartupInjectionAssemblyName.Startup).Assembly); + + // Assert + Assert.Equal(FactoryResolutionResultKind.Success, resolverResult.ResultKind); + Assert.NotNull(resolverResult.WebHostBuilderFactory); + Assert.NotNull(resolverResult.WebHostFactory); + Assert.IsAssignableFrom<IWebHostBuilder>(resolverResult.WebHostBuilderFactory(Array.Empty<string>())); + } + + [Fact] + public void CanFindWebHost_CreateWebHostBuilderPattern() + { + // Arrange & Act + var resolverResult = WebHostFactoryResolver.ResolveWebHostFactory<IWebHost, IWebHostBuilder>(typeof(IStartupInjectionAssemblyName.Startup).Assembly); + + // Assert + Assert.Equal(FactoryResolutionResultKind.Success, resolverResult.ResultKind); + Assert.NotNull(resolverResult.WebHostBuilderFactory); + Assert.NotNull(resolverResult.WebHostFactory); + } + + [Fact] + public void CanNotFindWebHostBuilder_BuildWebHostPattern() + { + // Arrange & Act + var resolverResult = WebHostFactoryResolver.ResolveWebHostBuilderFactory<IWebHost, IWebHostBuilder>(typeof(BuildWebHostPatternTestSite.Startup).Assembly); + + // Assert + Assert.Equal(FactoryResolutionResultKind.NoCreateWebHostBuilder, resolverResult.ResultKind); + Assert.Null(resolverResult.WebHostBuilderFactory); + Assert.Null(resolverResult.WebHostFactory); + } + + [Fact] + public void CanNotFindWebHostBuilder_CreateWebHostBuilderIncorrectSignature() + { + // Arrange & Act + var resolverResult = WebHostFactoryResolver.ResolveWebHostBuilderFactory<IWebHost, IWebHostBuilder>(typeof(CreateWebHostBuilderInvalidSignature.Startup).Assembly); + + // Assert + Assert.Equal(FactoryResolutionResultKind.NoCreateWebHostBuilder, resolverResult.ResultKind); + Assert.Null(resolverResult.WebHostBuilderFactory); + Assert.Null(resolverResult.WebHostFactory); + } + + [Fact] + public void CanNotFindWebHost_BuildWebHostIncorrectSignature() + { + // Arrange & Act + var resolverResult = WebHostFactoryResolver.ResolveWebHostFactory<IWebHost, IWebHostBuilder>(typeof(BuildWebHostInvalidSignature.Startup).Assembly); + + // Assert + Assert.Equal(FactoryResolutionResultKind.NoBuildWebHost, resolverResult.ResultKind); + Assert.Null(resolverResult.WebHostBuilderFactory); + Assert.Null(resolverResult.WebHostFactory); + } + + [Fact] + public void CanFindWebHost_BuildWebHostPattern() + { + // Arrange & Act + var resolverResult = WebHostFactoryResolver.ResolveWebHostFactory<IWebHost, IWebHostBuilder>(typeof(BuildWebHostPatternTestSite.Startup).Assembly); + + // Assert + Assert.Equal(FactoryResolutionResultKind.Success, resolverResult.ResultKind); + Assert.Null(resolverResult.WebHostBuilderFactory); + Assert.NotNull(resolverResult.WebHostFactory); + } + } +} diff --git a/src/Hosting/test/testassets/BuildWebHostInvalidSignature/BuildWebHostInvalidSignature.csproj b/src/Hosting/test/testassets/BuildWebHostInvalidSignature/BuildWebHostInvalidSignature.csproj new file mode 100644 index 0000000000000000000000000000000000000000..40bd0c87a314e494a7904f2781359d8aec69ab61 --- /dev/null +++ b/src/Hosting/test/testassets/BuildWebHostInvalidSignature/BuildWebHostInvalidSignature.csproj @@ -0,0 +1,14 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>netcoreapp2.1;netcoreapp2.0;net461</TargetFrameworks> + <OutputType>Exe</OutputType> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Hosting" /> + <Reference Include="Microsoft.AspNetCore.TestHost" /> + <Reference Include="Microsoft.Extensions.DependencyInjection" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/test/testassets/BuildWebHostInvalidSignature/Program.cs b/src/Hosting/test/testassets/BuildWebHostInvalidSignature/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..b25cb703ad097af2801763cfa7b42f256667652d --- /dev/null +++ b/src/Hosting/test/testassets/BuildWebHostInvalidSignature/Program.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Hosting; + +namespace BuildWebHostInvalidSignature +{ + class Program + { + static void Main(string[] args) + { + } + + public static IWebHost BuildWebHost() => null; + } +} diff --git a/src/Hosting/test/testassets/BuildWebHostInvalidSignature/Startup.cs b/src/Hosting/test/testassets/BuildWebHostInvalidSignature/Startup.cs new file mode 100644 index 0000000000000000000000000000000000000000..0b52bc36b875a15c522229a93e047415d1c38042 --- /dev/null +++ b/src/Hosting/test/testassets/BuildWebHostInvalidSignature/Startup.cs @@ -0,0 +1,19 @@ +// 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.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace BuildWebHostInvalidSignature +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder builder) + { + } + } +} diff --git a/src/Hosting/test/testassets/BuildWebHostPatternTestSite/BuildWebHostPatternTestSite.csproj b/src/Hosting/test/testassets/BuildWebHostPatternTestSite/BuildWebHostPatternTestSite.csproj new file mode 100644 index 0000000000000000000000000000000000000000..40bd0c87a314e494a7904f2781359d8aec69ab61 --- /dev/null +++ b/src/Hosting/test/testassets/BuildWebHostPatternTestSite/BuildWebHostPatternTestSite.csproj @@ -0,0 +1,14 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>netcoreapp2.1;netcoreapp2.0;net461</TargetFrameworks> + <OutputType>Exe</OutputType> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Hosting" /> + <Reference Include="Microsoft.AspNetCore.TestHost" /> + <Reference Include="Microsoft.Extensions.DependencyInjection" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/test/testassets/BuildWebHostPatternTestSite/Program.cs b/src/Hosting/test/testassets/BuildWebHostPatternTestSite/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..a5a20508b9e09f3f1c4a4c324ab7e22ae349ae0b --- /dev/null +++ b/src/Hosting/test/testassets/BuildWebHostPatternTestSite/Program.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Hosting; + +namespace BuildWebHostPatternTestSite +{ + class Program + { + static void Main(string[] args) + { + } + + public static IWebHost BuildWebHost(string[] args) => null; + } +} diff --git a/src/Hosting/test/testassets/BuildWebHostPatternTestSite/Startup.cs b/src/Hosting/test/testassets/BuildWebHostPatternTestSite/Startup.cs new file mode 100644 index 0000000000000000000000000000000000000000..10ecf07a99af3771263b84b39a0eebf37d2fea06 --- /dev/null +++ b/src/Hosting/test/testassets/BuildWebHostPatternTestSite/Startup.cs @@ -0,0 +1,19 @@ +// 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.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace BuildWebHostPatternTestSite +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder builder) + { + } + } +} diff --git a/src/Hosting/test/testassets/CreateWebHostBuilderInvalidSignature/CreateWebHostBuilderInvalidSignature.csproj b/src/Hosting/test/testassets/CreateWebHostBuilderInvalidSignature/CreateWebHostBuilderInvalidSignature.csproj new file mode 100644 index 0000000000000000000000000000000000000000..40bd0c87a314e494a7904f2781359d8aec69ab61 --- /dev/null +++ b/src/Hosting/test/testassets/CreateWebHostBuilderInvalidSignature/CreateWebHostBuilderInvalidSignature.csproj @@ -0,0 +1,14 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>netcoreapp2.1;netcoreapp2.0;net461</TargetFrameworks> + <OutputType>Exe</OutputType> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Hosting" /> + <Reference Include="Microsoft.AspNetCore.TestHost" /> + <Reference Include="Microsoft.Extensions.DependencyInjection" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/test/testassets/CreateWebHostBuilderInvalidSignature/Program.cs b/src/Hosting/test/testassets/CreateWebHostBuilderInvalidSignature/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..9322836fe0a313cb74bdb7a4f9c0d5782302f804 --- /dev/null +++ b/src/Hosting/test/testassets/CreateWebHostBuilderInvalidSignature/Program.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Hosting; + +namespace CreateWebHostBuilderInvalidSignature +{ + class Program + { + static void Main(string[] args) + { + } + + public static IWebHostBuilder CreateWebHostBuilder() => null; + } +} diff --git a/src/Hosting/test/testassets/CreateWebHostBuilderInvalidSignature/Startup.cs b/src/Hosting/test/testassets/CreateWebHostBuilderInvalidSignature/Startup.cs new file mode 100644 index 0000000000000000000000000000000000000000..2655a7e9374b2aa9f6bc1f555fd607820dad218a --- /dev/null +++ b/src/Hosting/test/testassets/CreateWebHostBuilderInvalidSignature/Startup.cs @@ -0,0 +1,19 @@ +// 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.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace CreateWebHostBuilderInvalidSignature +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder builder) + { + } + } +} diff --git a/src/Hosting/test/testassets/IStartupInjectionAssemblyName/IStartupInjectionAssemblyName.csproj b/src/Hosting/test/testassets/IStartupInjectionAssemblyName/IStartupInjectionAssemblyName.csproj new file mode 100644 index 0000000000000000000000000000000000000000..40bd0c87a314e494a7904f2781359d8aec69ab61 --- /dev/null +++ b/src/Hosting/test/testassets/IStartupInjectionAssemblyName/IStartupInjectionAssemblyName.csproj @@ -0,0 +1,14 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>netcoreapp2.1;netcoreapp2.0;net461</TargetFrameworks> + <OutputType>Exe</OutputType> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Hosting" /> + <Reference Include="Microsoft.AspNetCore.TestHost" /> + <Reference Include="Microsoft.Extensions.DependencyInjection" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/test/testassets/IStartupInjectionAssemblyName/Program.cs b/src/Hosting/test/testassets/IStartupInjectionAssemblyName/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..405ec1fba0e2d641070453ff00ce179a134ecbe4 --- /dev/null +++ b/src/Hosting/test/testassets/IStartupInjectionAssemblyName/Program.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; + +namespace IStartupInjectionAssemblyName +{ + public class Program + { + public static void Main(string[] args) + { + var webHost = CreateWebHostBuilder(args).Build(); + var applicationName = webHost.Services.GetRequiredService<IHostingEnvironment>().ApplicationName; + Console.WriteLine(applicationName); + Console.ReadKey(); + } + + // Do not change the signature of this method. It's used for tests. + private static IWebHostBuilder CreateWebHostBuilder(string [] args) => + new WebHostBuilder() + .SuppressStatusMessages(true) + .ConfigureServices(services => services.AddSingleton<IStartup, Startup>()); + } +} diff --git a/src/Hosting/test/testassets/IStartupInjectionAssemblyName/Startup.cs b/src/Hosting/test/testassets/IStartupInjectionAssemblyName/Startup.cs new file mode 100644 index 0000000000000000000000000000000000000000..9f4e27223cedbe53226fe6671a9311c99c3af47a --- /dev/null +++ b/src/Hosting/test/testassets/IStartupInjectionAssemblyName/Startup.cs @@ -0,0 +1,21 @@ + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Http; + +namespace IStartupInjectionAssemblyName +{ + public class Startup : IStartup + { + public void Configure(IApplicationBuilder app) + { + } + + public IServiceProvider ConfigureServices(IServiceCollection services) + { + return services.BuildServiceProvider(); + } + } +} diff --git a/src/Hosting/test/testassets/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj b/src/Hosting/test/testassets/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj new file mode 100644 index 0000000000000000000000000000000000000000..7b87b0eed54905d1f82ffdf720b9d8a975f143b3 --- /dev/null +++ b/src/Hosting/test/testassets/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj @@ -0,0 +1,16 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>netcoreapp2.1;netcoreapp2.0;net461</TargetFrameworks> + <OutputType>Exe</OutputType> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Hosting" /> + <Reference Include="Microsoft.Extensions.Configuration" /> + <Reference Include="Microsoft.Extensions.Configuration.CommandLine" /> + <Reference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" /> + <Reference Include="Microsoft.Extensions.Logging.Console" /> + </ItemGroup> + +</Project> diff --git a/src/Hosting/test/testassets/Microsoft.AspNetCore.Hosting.TestSites/Program.cs b/src/Hosting/test/testassets/Microsoft.AspNetCore.Hosting.TestSites/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..36056bff482180dc2e669093c7b8a18d46379861 --- /dev/null +++ b/src/Hosting/test/testassets/Microsoft.AspNetCore.Hosting.TestSites/Program.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; + +namespace ServerComparison.TestSites +{ + public static class Program + { + public static void Main(string[] args) + { + var config = new ConfigurationBuilder() + .AddCommandLine(args) + .AddEnvironmentVariables(prefix: "ASPNETCORE_") + .Build(); + + var builder = new WebHostBuilder() + .UseServer(new NoopServer()) + .UseConfiguration(config) + .SuppressStatusMessages(true) + .ConfigureLogging((_, factory) => + { + factory.AddConsole(); + factory.AddFilter<ConsoleLoggerProvider>(level => level >= LogLevel.Warning); + }) + .UseStartup("Microsoft.AspNetCore.Hosting.TestSites"); + + if (config["STARTMECHANIC"] == "Run") + { + var host = builder.Build(); + + host.Run(); + } + else if (config["STARTMECHANIC"] == "WaitForShutdown") + { + using (var host = builder.Build()) + { + host.Start(); + + host.WaitForShutdown(); + } + } + else + { + throw new InvalidOperationException("Starting mechanic not specified"); + } + } + } + + public class NoopServer : IServer + { + public void Dispose() + { + } + + public IFeatureCollection Features { get; } = new FeatureCollection(); + + public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} + diff --git a/src/Hosting/test/testassets/Microsoft.AspNetCore.Hosting.TestSites/StartupShutdown.cs b/src/Hosting/test/testassets/Microsoft.AspNetCore.Hosting.TestSites/StartupShutdown.cs new file mode 100644 index 0000000000000000000000000000000000000000..8b223d9e5a6fe7a4fcd07144aa14ed02d7db7607 --- /dev/null +++ b/src/Hosting/test/testassets/Microsoft.AspNetCore.Hosting.TestSites/StartupShutdown.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Hosting.TestSites +{ + public class StartupShutdown + { + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory, IApplicationLifetime lifetime) + { + lifetime.ApplicationStarted.Register(() => + { + Console.WriteLine("Started"); + }); + lifetime.ApplicationStopping.Register(() => + { + Console.WriteLine("Stopping firing"); + System.Threading.Thread.Sleep(200); + Console.WriteLine("Stopping end"); + }); + lifetime.ApplicationStopped.Register(() => + { + Console.WriteLine("Stopped firing"); + System.Threading.Thread.Sleep(200); + Console.WriteLine("Stopped end"); + }); + + app.Run(context => + { + return context.Response.WriteAsync("Hello World"); + }); + } + } +} diff --git a/src/Hosting/test/testassets/TestStartupAssembly1/TestHostingStartup1.cs b/src/Hosting/test/testassets/TestStartupAssembly1/TestHostingStartup1.cs new file mode 100644 index 0000000000000000000000000000000000000000..e8519c83c36c35cd9d5396fbf97360c6bea8955b --- /dev/null +++ b/src/Hosting/test/testassets/TestStartupAssembly1/TestHostingStartup1.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Hosting; + +[assembly: HostingStartup(typeof(TestStartupAssembly1.TestHostingStartup1))] + +namespace TestStartupAssembly1 +{ + public class TestHostingStartup1 : IHostingStartup + { + public void Configure(IWebHostBuilder builder) + { + builder.UseSetting("testhostingstartup1", "1"); + builder.UseSetting("testhostingstartup_chain", builder.GetSetting("testhostingstartup_chain") + "1"); + } + } +} diff --git a/src/Hosting/test/testassets/TestStartupAssembly1/TestStartupAssembly1.csproj b/src/Hosting/test/testassets/TestStartupAssembly1/TestStartupAssembly1.csproj new file mode 100644 index 0000000000000000000000000000000000000000..951d8c69e3bcf3e1f1831835716d1aaca7bd10ce --- /dev/null +++ b/src/Hosting/test/testassets/TestStartupAssembly1/TestStartupAssembly1.csproj @@ -0,0 +1,11 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" /> + </ItemGroup> + +</Project> \ No newline at end of file diff --git a/src/Http/Authentication.Abstractions/src/AuthenticateResult.cs b/src/Http/Authentication.Abstractions/src/AuthenticateResult.cs new file mode 100644 index 0000000000000000000000000000000000000000..5982143bcb9a1abb8fcb1878e0abcb66ce66b2da --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticateResult.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Contains the result of an Authenticate call + /// </summary> + public class AuthenticateResult + { + protected AuthenticateResult() { } + + /// <summary> + /// If a ticket was produced, authenticate was successful. + /// </summary> + public bool Succeeded => Ticket != null; + + /// <summary> + /// The authentication ticket. + /// </summary> + public AuthenticationTicket Ticket { get; protected set; } + + /// <summary> + /// Gets the claims-principal with authenticated user identities. + /// </summary> + public ClaimsPrincipal Principal => Ticket?.Principal; + + /// <summary> + /// Additional state values for the authentication session. + /// </summary> + public AuthenticationProperties Properties { get; protected set; } + + /// <summary> + /// Holds failure information from the authentication. + /// </summary> + public Exception Failure { get; protected set; } + + /// <summary> + /// Indicates that there was no information returned for this authentication scheme. + /// </summary> + public bool None { get; protected set; } + + /// <summary> + /// Indicates that authentication was successful. + /// </summary> + /// <param name="ticket">The ticket representing the authentication result.</param> + /// <returns>The result.</returns> + public static AuthenticateResult Success(AuthenticationTicket ticket) + { + if (ticket == null) + { + throw new ArgumentNullException(nameof(ticket)); + } + return new AuthenticateResult() { Ticket = ticket, Properties = ticket.Properties }; + } + + /// <summary> + /// Indicates that there was no information returned for this authentication scheme. + /// </summary> + /// <returns>The result.</returns> + public static AuthenticateResult NoResult() + { + return new AuthenticateResult() { None = true }; + } + + /// <summary> + /// Indicates that there was a failure during authentication. + /// </summary> + /// <param name="failure">The failure exception.</param> + /// <returns>The result.</returns> + public static AuthenticateResult Fail(Exception failure) + { + return new AuthenticateResult() { Failure = failure }; + } + + /// <summary> + /// Indicates that there was a failure during authentication. + /// </summary> + /// <param name="failure">The failure exception.</param> + /// <param name="properties">Additional state values for the authentication session.</param> + /// <returns>The result.</returns> + public static AuthenticateResult Fail(Exception failure, AuthenticationProperties properties) + { + return new AuthenticateResult() { Failure = failure, Properties = properties }; + } + + /// <summary> + /// Indicates that there was a failure during authentication. + /// </summary> + /// <param name="failureMessage">The failure message.</param> + /// <returns>The result.</returns> + public static AuthenticateResult Fail(string failureMessage) + => Fail(new Exception(failureMessage)); + + /// <summary> + /// Indicates that there was a failure during authentication. + /// </summary> + /// <param name="failureMessage">The failure message.</param> + /// <param name="properties">Additional state values for the authentication session.</param> + /// <returns>The result.</returns> + public static AuthenticateResult Fail(string failureMessage, AuthenticationProperties properties) + => Fail(new Exception(failureMessage), properties); + } +} diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationHttpContextExtensions.cs b/src/Http/Authentication.Abstractions/src/AuthenticationHttpContextExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..bb50c6534f5e1fa75e16e97b04303e2b32c7d300 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticationHttpContextExtensions.cs @@ -0,0 +1,197 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Extension methods to expose Authentication on HttpContext. + /// </summary> + public static class AuthenticationHttpContextExtensions + { + /// <summary> + /// Extension method for authenticate using the <see cref="AuthenticationOptions.DefaultAuthenticateScheme"/> scheme. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <returns>The <see cref="AuthenticateResult"/>.</returns> + public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context) => + context.AuthenticateAsync(scheme: null); + + /// <summary> + /// Extension method for authenticate. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <returns>The <see cref="AuthenticateResult"/>.</returns> + public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) => + context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme); + + /// <summary> + /// Extension method for Challenge. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <returns>The result.</returns> + public static Task ChallengeAsync(this HttpContext context, string scheme) => + context.ChallengeAsync(scheme, properties: null); + + /// <summary> + /// Extension method for authenticate using the <see cref="AuthenticationOptions.DefaultChallengeScheme"/> scheme. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <returns>The task.</returns> + public static Task ChallengeAsync(this HttpContext context) => + context.ChallengeAsync(scheme: null, properties: null); + + /// <summary> + /// Extension method for authenticate using the <see cref="AuthenticationOptions.DefaultChallengeScheme"/> scheme. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param> + /// <returns>The task.</returns> + public static Task ChallengeAsync(this HttpContext context, AuthenticationProperties properties) => + context.ChallengeAsync(scheme: null, properties: properties); + + /// <summary> + /// Extension method for Challenge. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param> + /// <returns>The task.</returns> + public static Task ChallengeAsync(this HttpContext context, string scheme, AuthenticationProperties properties) => + context.RequestServices.GetRequiredService<IAuthenticationService>().ChallengeAsync(context, scheme, properties); + + /// <summary> + /// Extension method for Forbid. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <returns>The task.</returns> + public static Task ForbidAsync(this HttpContext context, string scheme) => + context.ForbidAsync(scheme, properties: null); + + /// <summary> + /// Extension method for Forbid using the <see cref="AuthenticationOptions.DefaultForbidScheme"/> scheme.. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <returns>The task.</returns> + public static Task ForbidAsync(this HttpContext context) => + context.ForbidAsync(scheme: null, properties: null); + + /// <summary> + /// Extension method for Forbid. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param> + /// <returns>The task.</returns> + public static Task ForbidAsync(this HttpContext context, AuthenticationProperties properties) => + context.ForbidAsync(scheme: null, properties: properties); + + /// <summary> + /// Extension method for Forbid. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param> + /// <returns>The task.</returns> + public static Task ForbidAsync(this HttpContext context, string scheme, AuthenticationProperties properties) => + context.RequestServices.GetRequiredService<IAuthenticationService>().ForbidAsync(context, scheme, properties); + + /// <summary> + /// Extension method for SignIn. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="principal">The user.</param> + /// <returns>The task.</returns> + public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal) => + context.SignInAsync(scheme, principal, properties: null); + + /// <summary> + /// Extension method for SignIn using the <see cref="AuthenticationOptions.DefaultSignInScheme"/>. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="principal">The user.</param> + /// <returns>The task.</returns> + public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal) => + context.SignInAsync(scheme: null, principal: principal, properties: null); + + /// <summary> + /// Extension method for SignIn using the <see cref="AuthenticationOptions.DefaultSignInScheme"/>. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="principal">The user.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param> + /// <returns>The task.</returns> + public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal, AuthenticationProperties properties) => + context.SignInAsync(scheme: null, principal: principal, properties: properties); + + /// <summary> + /// Extension method for SignIn. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="principal">The user.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param> + /// <returns>The task.</returns> + public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) => + context.RequestServices.GetRequiredService<IAuthenticationService>().SignInAsync(context, scheme, principal, properties); + + /// <summary> + /// Extension method for SignOut using the <see cref="AuthenticationOptions.DefaultSignOutScheme"/>. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <returns>The task.</returns> + public static Task SignOutAsync(this HttpContext context) => context.SignOutAsync(scheme: null, properties: null); + + /// <summary> + /// Extension method for SignOut using the <see cref="AuthenticationOptions.DefaultSignOutScheme"/>. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param> + /// <returns>The task.</returns> + public static Task SignOutAsync(this HttpContext context, AuthenticationProperties properties) => context.SignOutAsync(scheme: null, properties: properties); + + /// <summary> + /// Extension method for SignOut. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <returns>The task.</returns> + public static Task SignOutAsync(this HttpContext context, string scheme) => context.SignOutAsync(scheme, properties: null); + + /// <summary> + /// Extension method for SignOut. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param> + /// <returns></returns> + public static Task SignOutAsync(this HttpContext context, string scheme, AuthenticationProperties properties) => + context.RequestServices.GetRequiredService<IAuthenticationService>().SignOutAsync(context, scheme, properties); + + /// <summary> + /// Extension method for getting the value of an authentication token. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="tokenName">The name of the token.</param> + /// <returns>The value of the token.</returns> + public static Task<string> GetTokenAsync(this HttpContext context, string scheme, string tokenName) => + context.RequestServices.GetRequiredService<IAuthenticationService>().GetTokenAsync(context, scheme, tokenName); + + /// <summary> + /// Extension method for getting the value of an authentication token. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="tokenName">The name of the token.</param> + /// <returns>The value of the token.</returns> + public static Task<string> GetTokenAsync(this HttpContext context, string tokenName) => + context.RequestServices.GetRequiredService<IAuthenticationService>().GetTokenAsync(context, tokenName); + } +} diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationOptions.cs b/src/Http/Authentication.Abstractions/src/AuthenticationOptions.cs new file mode 100644 index 0000000000000000000000000000000000000000..2781a35757060caf3c7ef590e24d296ef03f0d1a --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticationOptions.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + public class AuthenticationOptions + { + private readonly IList<AuthenticationSchemeBuilder> _schemes = new List<AuthenticationSchemeBuilder>(); + + /// <summary> + /// Returns the schemes in the order they were added (important for request handling priority) + /// </summary> + public IEnumerable<AuthenticationSchemeBuilder> Schemes => _schemes; + + /// <summary> + /// Maps schemes by name. + /// </summary> + public IDictionary<string, AuthenticationSchemeBuilder> SchemeMap { get; } = new Dictionary<string, AuthenticationSchemeBuilder>(StringComparer.Ordinal); + + /// <summary> + /// Adds an <see cref="AuthenticationScheme"/>. + /// </summary> + /// <param name="name">The name of the scheme being added.</param> + /// <param name="configureBuilder">Configures the scheme.</param> + public void AddScheme(string name, Action<AuthenticationSchemeBuilder> configureBuilder) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + if (configureBuilder == null) + { + throw new ArgumentNullException(nameof(configureBuilder)); + } + if (SchemeMap.ContainsKey(name)) + { + throw new InvalidOperationException("Scheme already exists: " + name); + } + + var builder = new AuthenticationSchemeBuilder(name); + configureBuilder(builder); + _schemes.Add(builder); + SchemeMap[name] = builder; + } + + /// <summary> + /// Adds an <see cref="AuthenticationScheme"/>. + /// </summary> + /// <typeparam name="THandler">The <see cref="IAuthenticationHandler"/> responsible for the scheme.</typeparam> + /// <param name="name">The name of the scheme being added.</param> + /// <param name="displayName">The display name for the scheme.</param> + public void AddScheme<THandler>(string name, string displayName) where THandler : IAuthenticationHandler + => AddScheme(name, b => + { + b.DisplayName = displayName; + b.HandlerType = typeof(THandler); + }); + + /// <summary> + /// Used as the fallback default scheme for all the other defaults. + /// </summary> + public string DefaultScheme { get; set; } + + /// <summary> + /// Used as the default scheme by <see cref="IAuthenticationService.AuthenticateAsync(HttpContext, string)"/>. + /// </summary> + public string DefaultAuthenticateScheme { get; set; } + + /// <summary> + /// Used as the default scheme by <see cref="IAuthenticationService.SignInAsync(HttpContext, string, System.Security.Claims.ClaimsPrincipal, AuthenticationProperties)"/>. + /// </summary> + public string DefaultSignInScheme { get; set; } + + /// <summary> + /// Used as the default scheme by <see cref="IAuthenticationService.SignOutAsync(HttpContext, string, AuthenticationProperties)"/>. + /// </summary> + public string DefaultSignOutScheme { get; set; } + + /// <summary> + /// Used as the default scheme by <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>. + /// </summary> + public string DefaultChallengeScheme { get; set; } + + /// <summary> + /// Used as the default scheme by <see cref="IAuthenticationService.ForbidAsync(HttpContext, string, AuthenticationProperties)"/>. + /// </summary> + public string DefaultForbidScheme { get; set; } + } +} diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs b/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs new file mode 100644 index 0000000000000000000000000000000000000000..271329209a13ba5d8fdc02448224dc02b47cd695 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs @@ -0,0 +1,212 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Dictionary used to store state values about the authentication session. + /// </summary> + public class AuthenticationProperties + { + internal const string IssuedUtcKey = ".issued"; + internal const string ExpiresUtcKey = ".expires"; + internal const string IsPersistentKey = ".persistent"; + internal const string RedirectUriKey = ".redirect"; + internal const string RefreshKey = ".refresh"; + internal const string UtcDateTimeFormat = "r"; + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationProperties"/> class. + /// </summary> + public AuthenticationProperties() + : this(items: null, parameters: null) + { } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationProperties"/> class. + /// </summary> + /// <param name="items">State values dictionary to use.</param> + public AuthenticationProperties(IDictionary<string, string> items) + : this(items, parameters: null) + { } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationProperties"/> class. + /// </summary> + /// <param name="items">State values dictionary to use.</param> + /// <param name="parameters">Parameters dictionary to use.</param> + public AuthenticationProperties(IDictionary<string, string> items, IDictionary<string, object> parameters) + { + Items = items ?? new Dictionary<string, string>(StringComparer.Ordinal); + Parameters = parameters ?? new Dictionary<string, object>(StringComparer.Ordinal); + } + + /// <summary> + /// State values about the authentication session. + /// </summary> + public IDictionary<string, string> Items { get; } + + /// <summary> + /// Collection of parameters that are passed to the authentication handler. These are not intended for + /// serialization or persistence, only for flowing data between call sites. + /// </summary> + public IDictionary<string, object> Parameters { get; } + + /// <summary> + /// Gets or sets whether the authentication session is persisted across multiple requests. + /// </summary> + public bool IsPersistent + { + get => GetString(IsPersistentKey) != null; + set => SetString(IsPersistentKey, value ? string.Empty : null); + } + + /// <summary> + /// Gets or sets the full path or absolute URI to be used as an http redirect response value. + /// </summary> + public string RedirectUri + { + get => GetString(RedirectUriKey); + set => SetString(RedirectUriKey, value); + } + + /// <summary> + /// Gets or sets the time at which the authentication ticket was issued. + /// </summary> + public DateTimeOffset? IssuedUtc + { + get => GetDateTimeOffset(IssuedUtcKey); + set => SetDateTimeOffset(IssuedUtcKey, value); + } + + /// <summary> + /// Gets or sets the time at which the authentication ticket expires. + /// </summary> + public DateTimeOffset? ExpiresUtc + { + get => GetDateTimeOffset(ExpiresUtcKey); + set => SetDateTimeOffset(ExpiresUtcKey, value); + } + + /// <summary> + /// Gets or sets if refreshing the authentication session should be allowed. + /// </summary> + public bool? AllowRefresh + { + get => GetBool(RefreshKey); + set => SetBool(RefreshKey, value); + } + + /// <summary> + /// Get a string value from the <see cref="Items"/> collection. + /// </summary> + /// <param name="key">Property key.</param> + /// <returns>Retrieved value or <c>null</c> if the property is not set.</returns> + public string GetString(string key) + { + return Items.TryGetValue(key, out string value) ? value : null; + } + + /// <summary> + /// Set a string value in the <see cref="Items"/> collection. + /// </summary> + /// <param name="key">Property key.</param> + /// <param name="value">Value to set or <c>null</c> to remove the property.</param> + public void SetString(string key, string value) + { + if (value != null) + { + Items[key] = value; + } + else if (Items.ContainsKey(key)) + { + Items.Remove(key); + } + } + + /// <summary> + /// Get a parameter from the <see cref="Parameters"/> collection. + /// </summary> + /// <typeparam name="T">Parameter type.</typeparam> + /// <param name="key">Parameter key.</param> + /// <returns>Retrieved value or the default value if the property is not set.</returns> + public T GetParameter<T>(string key) + => Parameters.TryGetValue(key, out var obj) && obj is T value ? value : default; + + /// <summary> + /// Set a parameter value in the <see cref="Parameters"/> collection. + /// </summary> + /// <typeparam name="T">Parameter type.</typeparam> + /// <param name="key">Parameter key.</param> + /// <param name="value">Value to set.</param> + public void SetParameter<T>(string key, T value) + => Parameters[key] = value; + + /// <summary> + /// Get a bool value from the <see cref="Items"/> collection. + /// </summary> + /// <param name="key">Property key.</param> + /// <returns>Retrieved value or <c>null</c> if the property is not set.</returns> + protected bool? GetBool(string key) + { + if (Items.TryGetValue(key, out string value) && bool.TryParse(value, out bool boolValue)) + { + return boolValue; + } + return null; + } + + /// <summary> + /// Set a bool value in the <see cref="Items"/> collection. + /// </summary> + /// <param name="key">Property key.</param> + /// <param name="value">Value to set or <c>null</c> to remove the property.</param> + protected void SetBool(string key, bool? value) + { + if (value.HasValue) + { + Items[key] = value.Value.ToString(); + } + else if (Items.ContainsKey(key)) + { + Items.Remove(key); + } + } + + /// <summary> + /// Get a DateTimeOffset value from the <see cref="Items"/> collection. + /// </summary> + /// <param name="key">Property key.</param> + /// <returns>Retrieved value or <c>null</c> if the property is not set.</returns> + protected DateTimeOffset? GetDateTimeOffset(string key) + { + if (Items.TryGetValue(key, out string value) + && DateTimeOffset.TryParseExact(value, UtcDateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset dateTimeOffset)) + { + return dateTimeOffset; + } + return null; + } + + /// <summary> + /// Set a DateTimeOffset value in the <see cref="Items"/> collection. + /// </summary> + /// <param name="key">Property key.</param> + /// <param name="value">Value to set or <c>null</c> to remove the property.</param> + protected void SetDateTimeOffset(string key, DateTimeOffset? value) + { + if (value.HasValue) + { + Items[key] = value.Value.ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture); + } + else if (Items.ContainsKey(key)) + { + Items.Remove(key); + } + } + } +} diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationScheme.cs b/src/Http/Authentication.Abstractions/src/AuthenticationScheme.cs new file mode 100644 index 0000000000000000000000000000000000000000..a72dc893edd39d4033653493c4196a40b69744ee --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticationScheme.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// AuthenticationSchemes assign a name to a specific <see cref="IAuthenticationHandler"/> + /// handlerType. + /// </summary> + public class AuthenticationScheme + { + /// <summary> + /// Constructor. + /// </summary> + /// <param name="name">The name for the authentication scheme.</param> + /// <param name="displayName">The display name for the authentication scheme.</param> + /// <param name="handlerType">The <see cref="IAuthenticationHandler"/> type that handles this scheme.</param> + public AuthenticationScheme(string name, string displayName, Type handlerType) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + if (handlerType == null) + { + throw new ArgumentNullException(nameof(handlerType)); + } + if (!typeof(IAuthenticationHandler).IsAssignableFrom(handlerType)) + { + throw new ArgumentException("handlerType must implement IAuthenticationHandler."); + } + + Name = name; + HandlerType = handlerType; + DisplayName = displayName; + } + + /// <summary> + /// The name of the authentication scheme. + /// </summary> + public string Name { get; } + + /// <summary> + /// The display name for the scheme. Null is valid and used for non user facing schemes. + /// </summary> + public string DisplayName { get; } + + /// <summary> + /// The <see cref="IAuthenticationHandler"/> type that handles this scheme. + /// </summary> + public Type HandlerType { get; } + } +} diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationSchemeBuilder.cs b/src/Http/Authentication.Abstractions/src/AuthenticationSchemeBuilder.cs new file mode 100644 index 0000000000000000000000000000000000000000..30e843c02820530f1afe407ac9a238f5004c2d04 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticationSchemeBuilder.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Used to build <see cref="AuthenticationScheme"/>s. + /// </summary> + public class AuthenticationSchemeBuilder + { + /// <summary> + /// Constructor. + /// </summary> + /// <param name="name">The name of the scheme being built.</param> + public AuthenticationSchemeBuilder(string name) + { + Name = name; + } + + /// <summary> + /// The name of the scheme being built. + /// </summary> + public string Name { get; } + + /// <summary> + /// The display name for the scheme being built. + /// </summary> + public string DisplayName { get; set; } + + /// <summary> + /// The <see cref="IAuthenticationHandler"/> type responsible for this scheme. + /// </summary> + public Type HandlerType { get; set; } + + /// <summary> + /// Builds the <see cref="AuthenticationScheme"/> instance. + /// </summary> + /// <returns></returns> + public AuthenticationScheme Build() => new AuthenticationScheme(Name, DisplayName, HandlerType); + } +} diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationTicket.cs b/src/Http/Authentication.Abstractions/src/AuthenticationTicket.cs new file mode 100644 index 0000000000000000000000000000000000000000..c31f15ec014423f9c259b8a6ebd2695174fc0484 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticationTicket.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Contains user identity information as well as additional authentication state. + /// </summary> + public class AuthenticationTicket + { + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationTicket"/> class + /// </summary> + /// <param name="principal">the <see cref="ClaimsPrincipal"/> that represents the authenticated user.</param> + /// <param name="properties">additional properties that can be consumed by the user or runtime.</param> + /// <param name="authenticationScheme">the authentication middleware that was responsible for this ticket.</param> + public AuthenticationTicket(ClaimsPrincipal principal, AuthenticationProperties properties, string authenticationScheme) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + AuthenticationScheme = authenticationScheme; + Principal = principal; + Properties = properties ?? new AuthenticationProperties(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationTicket"/> class + /// </summary> + /// <param name="principal">the <see cref="ClaimsPrincipal"/> that represents the authenticated user.</param> + /// <param name="authenticationScheme">the authentication middleware that was responsible for this ticket.</param> + public AuthenticationTicket(ClaimsPrincipal principal, string authenticationScheme) + : this(principal, properties: null, authenticationScheme: authenticationScheme) + { } + + /// <summary> + /// Gets the authentication type. + /// </summary> + public string AuthenticationScheme { get; private set; } + + /// <summary> + /// Gets the claims-principal with authenticated user identities. + /// </summary> + public ClaimsPrincipal Principal { get; private set; } + + /// <summary> + /// Additional state values for the authentication session. + /// </summary> + public AuthenticationProperties Properties { get; private set; } + } +} diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs b/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs new file mode 100644 index 0000000000000000000000000000000000000000..555da9e098d452aea6f7994ca33fc918d29c1a37 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Name/Value representing an token. + /// </summary> + public class AuthenticationToken + { + /// <summary> + /// Name. + /// </summary> + public string Name { get; set; } + + /// <summary> + /// Value. + /// </summary> + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationFeature.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..43e5a13b4947fd5c78977bbb50da1facd4391e6c --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationFeature.cs @@ -0,0 +1,23 @@ +// 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.Authentication +{ + /// <summary> + /// Used to capture path info so redirects can be computed properly within an app.Map(). + /// </summary> + public interface IAuthenticationFeature + { + /// <summary> + /// The original path base. + /// </summary> + PathString OriginalPathBase { get; set; } + + /// <summary> + /// The original path. + /// </summary> + PathString OriginalPath { get; set; } + } +} diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationHandler.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..aeb373e18e7c381cef5d89f80ad2346ec38a724c --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationHandler.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Created per request to handle authentication for to a particular scheme. + /// </summary> + public interface IAuthenticationHandler + { + /// <summary> + /// The handler should initialize anything it needs from the request and scheme here. + /// </summary> + /// <param name="scheme">The <see cref="AuthenticationScheme"/> scheme.</param> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <returns></returns> + Task InitializeAsync(AuthenticationScheme scheme, HttpContext context); + + /// <summary> + /// Authentication behavior. + /// </summary> + /// <returns>The <see cref="AuthenticateResult"/> result.</returns> + Task<AuthenticateResult> AuthenticateAsync(); + + /// <summary> + /// Challenge behavior. + /// </summary> + /// <param name="properties">The <see cref="AuthenticationProperties"/> that contains the extra meta-data arriving with the authentication.</param> + /// <returns>A task.</returns> + Task ChallengeAsync(AuthenticationProperties properties); + + /// <summary> + /// Forbid behavior. + /// </summary> + /// <param name="properties">The <see cref="AuthenticationProperties"/> that contains the extra meta-data arriving with the authentication.</param> + /// <returns>A task.</returns> + Task ForbidAsync(AuthenticationProperties properties); + } +} diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationHandlerProvider.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationHandlerProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..0507f51d611dcc6b5c353143fdfa799b6e0a5ca9 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationHandlerProvider.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Provides the appropriate IAuthenticationHandler instance for the authenticationScheme and request. + /// </summary> + public interface IAuthenticationHandlerProvider + { + /// <summary> + /// Returns the handler instance that will be used. + /// </summary> + /// <param name="context">The context.</param> + /// <param name="authenticationScheme">The name of the authentication scheme being handled.</param> + /// <returns>The handler instance.</returns> + Task<IAuthenticationHandler> GetHandlerAsync(HttpContext context, string authenticationScheme); + } +} \ No newline at end of file diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationRequestHandler.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationRequestHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..fb1b227ad7a0ca2933b8bfacb278c369516e6cef --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationRequestHandler.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Used to determine if a handler wants to participate in request processing. + /// </summary> + public interface IAuthenticationRequestHandler : IAuthenticationHandler + { + /// <summary> + /// Returns true if request processing should stop. + /// </summary> + /// <returns></returns> + Task<bool> HandleRequestAsync(); + } + +} diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationSchemeProvider.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationSchemeProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..3d2584fca859e0acef9fbb242b5cc3c855c9d1d1 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationSchemeProvider.cs @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Responsible for managing what authenticationSchemes are supported. + /// </summary> + public interface IAuthenticationSchemeProvider + { + /// <summary> + /// Returns all currently registered <see cref="AuthenticationScheme"/>s. + /// </summary> + /// <returns>All currently registered <see cref="AuthenticationScheme"/>s.</returns> + Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync(); + + /// <summary> + /// Returns the <see cref="AuthenticationScheme"/> matching the name, or null. + /// </summary> + /// <param name="name">The name of the authenticationScheme.</param> + /// <returns>The scheme or null if not found.</returns> + Task<AuthenticationScheme> GetSchemeAsync(string name); + + /// <summary> + /// Returns the scheme that will be used by default for <see cref="IAuthenticationService.AuthenticateAsync(HttpContext, string)"/>. + /// This is typically specified via <see cref="AuthenticationOptions.DefaultAuthenticateScheme"/>. + /// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>. + /// </summary> + /// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.AuthenticateAsync(HttpContext, string)"/>.</returns> + Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync(); + + /// <summary> + /// Returns the scheme that will be used by default for <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>. + /// This is typically specified via <see cref="AuthenticationOptions.DefaultChallengeScheme"/>. + /// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>. + /// </summary> + /// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>.</returns> + Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync(); + + /// <summary> + /// Returns the scheme that will be used by default for <see cref="IAuthenticationService.ForbidAsync(HttpContext, string, AuthenticationProperties)"/>. + /// This is typically specified via <see cref="AuthenticationOptions.DefaultForbidScheme"/>. + /// Otherwise, this will fallback to <see cref="GetDefaultChallengeSchemeAsync"/> . + /// </summary> + /// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.ForbidAsync(HttpContext, string, AuthenticationProperties)"/>.</returns> + Task<AuthenticationScheme> GetDefaultForbidSchemeAsync(); + + /// <summary> + /// Returns the scheme that will be used by default for <see cref="IAuthenticationService.SignInAsync(HttpContext, string, System.Security.Claims.ClaimsPrincipal, AuthenticationProperties)"/>. + /// This is typically specified via <see cref="AuthenticationOptions.DefaultSignInScheme"/>. + /// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>. + /// </summary> + /// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.SignInAsync(HttpContext, string, System.Security.Claims.ClaimsPrincipal, AuthenticationProperties)"/>.</returns> + Task<AuthenticationScheme> GetDefaultSignInSchemeAsync(); + + /// <summary> + /// Returns the scheme that will be used by default for <see cref="IAuthenticationService.SignOutAsync(HttpContext, string, AuthenticationProperties)"/>. + /// This is typically specified via <see cref="AuthenticationOptions.DefaultSignOutScheme"/>. + /// Otherwise, this will fallback to <see cref="GetDefaultSignInSchemeAsync"/> . + /// </summary> + /// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.SignOutAsync(HttpContext, string, AuthenticationProperties)"/>.</returns> + Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync(); + + /// <summary> + /// Registers a scheme for use by <see cref="IAuthenticationService"/>. + /// </summary> + /// <param name="scheme">The scheme.</param> + void AddScheme(AuthenticationScheme scheme); + + /// <summary> + /// Removes a scheme, preventing it from being used by <see cref="IAuthenticationService"/>. + /// </summary> + /// <param name="name">The name of the authenticationScheme being removed.</param> + void RemoveScheme(string name); + + /// <summary> + /// Returns the schemes in priority order for request handling. + /// </summary> + /// <returns>The schemes in priority order for request handling</returns> + Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync(); + } +} \ No newline at end of file diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationService.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationService.cs new file mode 100644 index 0000000000000000000000000000000000000000..e5d53360160ebd19d2027fe6da419699af4e3ff6 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationService.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Used to provide authentication. + /// </summary> + public interface IAuthenticationService + { + /// <summary> + /// Authenticate for the specified authentication scheme. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/>.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <returns>The result.</returns> + Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme); + + /// <summary> + /// Challenge the specified authentication scheme. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/>.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> + /// <returns>A task.</returns> + Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties); + + /// <summary> + /// Forbids the specified authentication scheme. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/>.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> + /// <returns>A task.</returns> + Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties); + + /// <summary> + /// Sign a principal in for the specified authentication scheme. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/>.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="principal">The <see cref="ClaimsPrincipal"/> to sign in.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> + /// <returns>A task.</returns> + Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties); + + /// <summary> + /// Sign out the specified authentication scheme. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/>.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> + /// <returns>A task.</returns> + Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties); + } +} diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationSignInHandler.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationSignInHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..69b88032d5adecc7e93742b130edf28d5677a507 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationSignInHandler.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Used to determine if a handler supports SignIn. + /// </summary> + public interface IAuthenticationSignInHandler : IAuthenticationSignOutHandler + { + /// <summary> + /// Handle sign in. + /// </summary> + /// <param name="user">The <see cref="ClaimsPrincipal"/> user.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/> that contains the extra meta-data arriving with the authentication.</param> + /// <returns>A task.</returns> + Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties); + } +} diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationSignOutHandler.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationSignOutHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..f76d116a7602b4281102cc5b226261b03e7a2215 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationSignOutHandler.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Used to determine if a handler supports SignOut. + /// </summary> + public interface IAuthenticationSignOutHandler : IAuthenticationHandler + { + /// <summary> + /// Signout behavior. + /// </summary> + /// <param name="properties">The <see cref="AuthenticationProperties"/> that contains the extra meta-data arriving with the authentication.</param> + /// <returns>A task.</returns> + Task SignOutAsync(AuthenticationProperties properties); + } + +} diff --git a/src/Http/Authentication.Abstractions/src/IClaimsTransformation.cs b/src/Http/Authentication.Abstractions/src/IClaimsTransformation.cs new file mode 100644 index 0000000000000000000000000000000000000000..0193d957838c58c759ac078c15add3b28bba00f4 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IClaimsTransformation.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Used by the <see cref="IAuthenticationService"/> for claims transformation. + /// </summary> + public interface IClaimsTransformation + { + /// <summary> + /// Provides a central transformation point to change the specified principal. + /// Note: this will be run on each AuthenticateAsync call, so its safer to + /// return a new ClaimsPrincipal if your transformation is not idempotent. + /// </summary> + /// <param name="principal">The <see cref="ClaimsPrincipal"/> to transform.</param> + /// <returns>The transformed principal.</returns> + Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal); + } +} \ No newline at end of file diff --git a/src/Http/Authentication.Abstractions/src/Microsoft.AspNetCore.Authentication.Abstractions.csproj b/src/Http/Authentication.Abstractions/src/Microsoft.AspNetCore.Authentication.Abstractions.csproj new file mode 100644 index 0000000000000000000000000000000000000000..bfb6e8e9edd25e7dccc2987ae9e56b92dcf03695 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/Microsoft.AspNetCore.Authentication.Abstractions.csproj @@ -0,0 +1,17 @@ +<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> + <PropertyGroup> + <Description>ASP.NET Core common types used by the various authentication components.</Description> + <TargetFramework>netstandard2.0</TargetFramework> + <NoWarn>$(NoWarn);CS1591</NoWarn> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore;authentication;security</PackageTags> + <EnableApiCheck>false</EnableApiCheck> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Http.Abstractions" /> + <Reference Include="Microsoft.Extensions.Logging.Abstractions" /> + <Reference Include="Microsoft.Extensions.Options" /> + </ItemGroup> + +</Project> \ No newline at end of file diff --git a/src/Http/Authentication.Abstractions/src/TokenExtensions.cs b/src/Http/Authentication.Abstractions/src/TokenExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..497acabc23316c64bd54215b68e709231cb7a375 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/TokenExtensions.cs @@ -0,0 +1,161 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Extension methods for storing authentication tokens in <see cref="AuthenticationProperties"/>. + /// </summary> + public static class AuthenticationTokenExtensions + { + private static string TokenNamesKey = ".TokenNames"; + private static string TokenKeyPrefix = ".Token."; + + /// <summary> + /// Stores a set of authentication tokens, after removing any old tokens. + /// </summary> + /// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param> + /// <param name="tokens">The tokens to store.</param> + public static void StoreTokens(this AuthenticationProperties properties, IEnumerable<AuthenticationToken> tokens) + { + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } + if (tokens == null) + { + throw new ArgumentNullException(nameof(tokens)); + } + + // Clear old tokens first + var oldTokens = properties.GetTokens(); + foreach (var t in oldTokens) + { + properties.Items.Remove(TokenKeyPrefix + t.Name); + } + properties.Items.Remove(TokenNamesKey); + + var tokenNames = new List<string>(); + foreach (var token in tokens) + { + // REVIEW: should probably check that there are no ; in the token name and throw or encode + tokenNames.Add(token.Name); + properties.Items[TokenKeyPrefix+token.Name] = token.Value; + } + if (tokenNames.Count > 0) + { + properties.Items[TokenNamesKey] = string.Join(";", tokenNames.ToArray()); + } + } + + /// <summary> + /// Returns the value of a token. + /// </summary> + /// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param> + /// <param name="tokenName">The token name.</param> + /// <returns>The token value.</returns> + public static string GetTokenValue(this AuthenticationProperties properties, string tokenName) + { + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } + if (tokenName == null) + { + throw new ArgumentNullException(nameof(tokenName)); + } + + var tokenKey = TokenKeyPrefix + tokenName; + return properties.Items.ContainsKey(tokenKey) + ? properties.Items[tokenKey] + : null; + } + + public static bool UpdateTokenValue(this AuthenticationProperties properties, string tokenName, string tokenValue) + { + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } + if (tokenName == null) + { + throw new ArgumentNullException(nameof(tokenName)); + } + + var tokenKey = TokenKeyPrefix + tokenName; + if (!properties.Items.ContainsKey(tokenKey)) + { + return false; + } + properties.Items[tokenKey] = tokenValue; + return true; + } + + /// <summary> + /// Returns all of the AuthenticationTokens contained in the properties. + /// </summary> + /// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param> + /// <returns>The authentication toekns.</returns> + public static IEnumerable<AuthenticationToken> GetTokens(this AuthenticationProperties properties) + { + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } + + var tokens = new List<AuthenticationToken>(); + if (properties.Items.ContainsKey(TokenNamesKey)) + { + var tokenNames = properties.Items[TokenNamesKey].Split(';'); + foreach (var name in tokenNames) + { + var token = properties.GetTokenValue(name); + if (token != null) + { + tokens.Add(new AuthenticationToken { Name = name, Value = token }); + } + } + } + + return tokens; + } + + /// <summary> + /// Extension method for getting the value of an authentication token. + /// </summary> + /// <param name="auth">The <see cref="IAuthenticationService"/>.</param> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="tokenName">The name of the token.</param> + /// <returns>The value of the token.</returns> + public static Task<string> GetTokenAsync(this IAuthenticationService auth, HttpContext context, string tokenName) + => auth.GetTokenAsync(context, scheme: null, tokenName: tokenName); + + /// <summary> + /// Extension method for getting the value of an authentication token. + /// </summary> + /// <param name="auth">The <see cref="IAuthenticationService"/>.</param> + /// <param name="context">The <see cref="HttpContext"/> context.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="tokenName">The name of the token.</param> + /// <returns>The value of the token.</returns> + public static async Task<string> GetTokenAsync(this IAuthenticationService auth, HttpContext context, string scheme, string tokenName) + { + if (auth == null) + { + throw new ArgumentNullException(nameof(auth)); + } + if (tokenName == null) + { + throw new ArgumentNullException(nameof(tokenName)); + } + + var result = await auth.AuthenticateAsync(context, scheme); + return result?.Properties?.GetTokenValue(tokenName); + } + } +} \ No newline at end of file diff --git a/src/Http/Authentication.Abstractions/src/baseline.netcore.json b/src/Http/Authentication.Abstractions/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..2d1e7e00e4d8508bda90a74d5008faea579b65f8 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/baseline.netcore.json @@ -0,0 +1,1734 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticateResult", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Succeeded", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Ticket", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationTicket", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Ticket", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Failure", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Failure", + "Parameters": [ + { + "Name": "value", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_None", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_None", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Success", + "Parameters": [ + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticateResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "NoResult", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticateResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failure", + "Type": "System.Exception" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticateResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failureMessage", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticateResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTokenAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "tokenName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.String>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTokenAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "tokenName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.String>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Schemes", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authentication.AuthenticationSchemeBuilder>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SchemeMap", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, Microsoft.AspNetCore.Authentication.AuthenticationSchemeBuilder>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "configureBuilder", + "Type": "System.Action<Microsoft.AspNetCore.Authentication.AuthenticationSchemeBuilder>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddScheme<T0>", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "THandler", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "get_DefaultScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultAuthenticateScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultAuthenticateScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultSignInScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultSignInScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultSignOutScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultSignOutScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultChallengeScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultChallengeScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultForbidScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultForbidScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Items", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, System.String>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsPersistent", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsPersistent", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RedirectUri", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RedirectUri", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IssuedUtc", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IssuedUtc", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ExpiresUtc", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ExpiresUtc", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AllowRefresh", + "Parameters": [], + "ReturnType": "System.Nullable<System.Boolean>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AllowRefresh", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.Boolean>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary<System.String, System.String>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationScheme", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HandlerType", + "Parameters": [], + "ReturnType": "System.Type", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "handlerType", + "Type": "System.Type" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DisplayName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HandlerType", + "Parameters": [], + "ReturnType": "System.Type", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HandlerType", + "Parameters": [ + { + "Name": "value", + "Type": "System.Type" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationScheme", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationTicket", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationToken", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Name", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Value", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OriginalPathBase", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OriginalPathBase", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OriginalPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OriginalPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationHandler", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "InitializeAsync", + "Parameters": [ + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetHandlerAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.IAuthenticationHandler>", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "HandleRequestAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<System.Boolean>", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetAllSchemesAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authentication.AuthenticationScheme>>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetSchemeAsync", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultAuthenticateSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultChallengeSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultForbidSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultSignInSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultSignOutSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RemoveScheme", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetRequestHandlerSchemesAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authentication.AuthenticationScheme>>", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationService", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationSignInHandler", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IClaimsTransformation", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "TransformAsync", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Security.Claims.ClaimsPrincipal>", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationTokenExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "StoreTokens", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "tokens", + "Type": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authentication.AuthenticationToken>" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTokenValue", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "tokenName", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UpdateTokenValue", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "tokenName", + "Type": "System.String" + }, + { + "Name": "tokenValue", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTokens", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authentication.AuthenticationToken>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTokenAsync", + "Parameters": [ + { + "Name": "auth", + "Type": "Microsoft.AspNetCore.Authentication.IAuthenticationService" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "tokenName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.String>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTokenAsync", + "Parameters": [ + { + "Name": "auth", + "Type": "Microsoft.AspNetCore.Authentication.IAuthenticationService" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "tokenName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.String>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/Authentication.Core/src/AuthenticationCoreServiceCollectionExtensions.cs b/src/Http/Authentication.Core/src/AuthenticationCoreServiceCollectionExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..fdf85a9b45691ae082e069703d9f3a05072326c3 --- /dev/null +++ b/src/Http/Authentication.Core/src/AuthenticationCoreServiceCollectionExtensions.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// <summary> + /// Extension methods for setting up authentication services in an <see cref="IServiceCollection" />. + /// </summary> + public static class AuthenticationCoreServiceCollectionExtensions + { + /// <summary> + /// Add core authentication services needed for <see cref="IAuthenticationService"/>. + /// </summary> + /// <param name="services">The <see cref="IServiceCollection"/>.</param> + /// <returns>The service collection.</returns> + public static IServiceCollection AddAuthenticationCore(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAddScoped<IAuthenticationService, AuthenticationService>(); + services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>(); // Can be replaced with scoped ones that use DbContext + services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>(); + services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>(); + return services; + } + + /// <summary> + /// Add core authentication services needed for <see cref="IAuthenticationService"/>. + /// </summary> + /// <param name="services">The <see cref="IServiceCollection"/>.</param> + /// <param name="configureOptions">Used to configure the <see cref="AuthenticationOptions"/>.</param> + /// <returns>The service collection.</returns> + public static IServiceCollection AddAuthenticationCore(this IServiceCollection services, Action<AuthenticationOptions> configureOptions) { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + services.AddAuthenticationCore(); + services.Configure(configureOptions); + return services; + } + } +} diff --git a/src/Http/Authentication.Core/src/AuthenticationFeature.cs b/src/Http/Authentication.Core/src/AuthenticationFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..3282cbf4671fbc396d0dc93dab9ce542afefcf72 --- /dev/null +++ b/src/Http/Authentication.Core/src/AuthenticationFeature.cs @@ -0,0 +1,23 @@ +// 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.Authentication +{ + /// <summary> + /// Used to capture path info so redirects can be computed properly within an app.Map(). + /// </summary> + public class AuthenticationFeature : IAuthenticationFeature + { + /// <summary> + /// The original path base. + /// </summary> + public PathString OriginalPathBase { get; set; } + + /// <summary> + /// The original path. + /// </summary> + public PathString OriginalPath { get; set; } + } +} diff --git a/src/Http/Authentication.Core/src/AuthenticationHandlerProvider.cs b/src/Http/Authentication.Core/src/AuthenticationHandlerProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..c4921e533491e1f25c97154631e44d6eb7d64367 --- /dev/null +++ b/src/Http/Authentication.Core/src/AuthenticationHandlerProvider.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Implementation of <see cref="IAuthenticationHandlerProvider"/>. + /// </summary> + public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider + { + /// <summary> + /// Constructor. + /// </summary> + /// <param name="schemes">The <see cref="IAuthenticationHandlerProvider"/>.</param> + public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes) + { + Schemes = schemes; + } + + /// <summary> + /// The <see cref="IAuthenticationHandlerProvider"/>. + /// </summary> + public IAuthenticationSchemeProvider Schemes { get; } + + // handler instance cache, need to initialize once per request + private Dictionary<string, IAuthenticationHandler> _handlerMap = new Dictionary<string, IAuthenticationHandler>(StringComparer.Ordinal); + + /// <summary> + /// Returns the handler instance that will be used. + /// </summary> + /// <param name="context">The context.</param> + /// <param name="authenticationScheme">The name of the authentication scheme being handled.</param> + /// <returns>The handler instance.</returns> + public async Task<IAuthenticationHandler> GetHandlerAsync(HttpContext context, string authenticationScheme) + { + if (_handlerMap.ContainsKey(authenticationScheme)) + { + return _handlerMap[authenticationScheme]; + } + + var scheme = await Schemes.GetSchemeAsync(authenticationScheme); + if (scheme == null) + { + return null; + } + var handler = (context.RequestServices.GetService(scheme.HandlerType) ?? + ActivatorUtilities.CreateInstance(context.RequestServices, scheme.HandlerType)) + as IAuthenticationHandler; + if (handler != null) + { + await handler.InitializeAsync(scheme, context); + _handlerMap[authenticationScheme] = handler; + } + return handler; + } + } +} diff --git a/src/Http/Authentication.Core/src/AuthenticationSchemeProvider.cs b/src/Http/Authentication.Core/src/AuthenticationSchemeProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..050118d3c4386744a18f939dc54a649087162aa3 --- /dev/null +++ b/src/Http/Authentication.Core/src/AuthenticationSchemeProvider.cs @@ -0,0 +1,176 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Implements <see cref="IAuthenticationSchemeProvider"/>. + /// </summary> + public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider + { + /// <summary> + /// Creates an instance of <see cref="AuthenticationSchemeProvider"/> + /// using the specified <paramref name="options"/>, + /// </summary> + /// <param name="options">The <see cref="AuthenticationOptions"/> options.</param> + public AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options) + : this(options, new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal)) + { + } + + /// <summary> + /// Creates an instance of <see cref="AuthenticationSchemeProvider"/> + /// using the specified <paramref name="options"/> and <paramref name="schemes"/>. + /// </summary> + /// <param name="options">The <see cref="AuthenticationOptions"/> options.</param> + /// <param name="schemes">The dictionary used to store authentication schemes.</param> + protected AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes) + { + _options = options.Value; + + _schemes = schemes ?? throw new ArgumentNullException(nameof(schemes)); + _requestHandlers = new List<AuthenticationScheme>(); + + foreach (var builder in _options.Schemes) + { + var scheme = builder.Build(); + AddScheme(scheme); + } + } + + private readonly AuthenticationOptions _options; + private readonly object _lock = new object(); + + private readonly IDictionary<string, AuthenticationScheme> _schemes; + private readonly List<AuthenticationScheme> _requestHandlers; + + private Task<AuthenticationScheme> GetDefaultSchemeAsync() + => _options.DefaultScheme != null + ? GetSchemeAsync(_options.DefaultScheme) + : Task.FromResult<AuthenticationScheme>(null); + + /// <summary> + /// Returns the scheme that will be used by default for <see cref="IAuthenticationService.AuthenticateAsync(HttpContext, string)"/>. + /// This is typically specified via <see cref="AuthenticationOptions.DefaultAuthenticateScheme"/>. + /// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>. + /// </summary> + /// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.AuthenticateAsync(HttpContext, string)"/>.</returns> + public virtual Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync() + => _options.DefaultAuthenticateScheme != null + ? GetSchemeAsync(_options.DefaultAuthenticateScheme) + : GetDefaultSchemeAsync(); + + /// <summary> + /// Returns the scheme that will be used by default for <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>. + /// This is typically specified via <see cref="AuthenticationOptions.DefaultChallengeScheme"/>. + /// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>. + /// </summary> + /// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>.</returns> + public virtual Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync() + => _options.DefaultChallengeScheme != null + ? GetSchemeAsync(_options.DefaultChallengeScheme) + : GetDefaultSchemeAsync(); + + /// <summary> + /// Returns the scheme that will be used by default for <see cref="IAuthenticationService.ForbidAsync(HttpContext, string, AuthenticationProperties)"/>. + /// This is typically specified via <see cref="AuthenticationOptions.DefaultForbidScheme"/>. + /// Otherwise, this will fallback to <see cref="GetDefaultChallengeSchemeAsync"/> . + /// </summary> + /// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.ForbidAsync(HttpContext, string, AuthenticationProperties)"/>.</returns> + public virtual Task<AuthenticationScheme> GetDefaultForbidSchemeAsync() + => _options.DefaultForbidScheme != null + ? GetSchemeAsync(_options.DefaultForbidScheme) + : GetDefaultChallengeSchemeAsync(); + + /// <summary> + /// Returns the scheme that will be used by default for <see cref="IAuthenticationService.SignInAsync(HttpContext, string, System.Security.Claims.ClaimsPrincipal, AuthenticationProperties)"/>. + /// This is typically specified via <see cref="AuthenticationOptions.DefaultSignInScheme"/>. + /// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>. + /// </summary> + /// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.SignInAsync(HttpContext, string, System.Security.Claims.ClaimsPrincipal, AuthenticationProperties)"/>.</returns> + public virtual Task<AuthenticationScheme> GetDefaultSignInSchemeAsync() + => _options.DefaultSignInScheme != null + ? GetSchemeAsync(_options.DefaultSignInScheme) + : GetDefaultSchemeAsync(); + + /// <summary> + /// Returns the scheme that will be used by default for <see cref="IAuthenticationService.SignOutAsync(HttpContext, string, AuthenticationProperties)"/>. + /// This is typically specified via <see cref="AuthenticationOptions.DefaultSignOutScheme"/>. + /// Otherwise this will fallback to <see cref="GetDefaultSignInSchemeAsync"/> if that supoorts sign out. + /// </summary> + /// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.SignOutAsync(HttpContext, string, AuthenticationProperties)"/>.</returns> + public virtual Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync() + => _options.DefaultSignOutScheme != null + ? GetSchemeAsync(_options.DefaultSignOutScheme) + : GetDefaultSignInSchemeAsync(); + + /// <summary> + /// Returns the <see cref="AuthenticationScheme"/> matching the name, or null. + /// </summary> + /// <param name="name">The name of the authenticationScheme.</param> + /// <returns>The scheme or null if not found.</returns> + public virtual Task<AuthenticationScheme> GetSchemeAsync(string name) + => Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null); + + /// <summary> + /// Returns the schemes in priority order for request handling. + /// </summary> + /// <returns>The schemes in priority order for request handling</returns> + public virtual Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync() + => Task.FromResult<IEnumerable<AuthenticationScheme>>(_requestHandlers); + + /// <summary> + /// Registers a scheme for use by <see cref="IAuthenticationService"/>. + /// </summary> + /// <param name="scheme">The scheme.</param> + public virtual void AddScheme(AuthenticationScheme scheme) + { + if (_schemes.ContainsKey(scheme.Name)) + { + throw new InvalidOperationException("Scheme already exists: " + scheme.Name); + } + lock (_lock) + { + if (_schemes.ContainsKey(scheme.Name)) + { + throw new InvalidOperationException("Scheme already exists: " + scheme.Name); + } + if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType)) + { + _requestHandlers.Add(scheme); + } + _schemes[scheme.Name] = scheme; + } + } + + /// <summary> + /// Removes a scheme, preventing it from being used by <see cref="IAuthenticationService"/>. + /// </summary> + /// <param name="name">The name of the authenticationScheme being removed.</param> + public virtual void RemoveScheme(string name) + { + if (!_schemes.ContainsKey(name)) + { + return; + } + lock (_lock) + { + if (_schemes.ContainsKey(name)) + { + var scheme = _schemes[name]; + _requestHandlers.Remove(scheme); + _schemes.Remove(name); + } + } + } + + public virtual Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync() + => Task.FromResult<IEnumerable<AuthenticationScheme>>(_schemes.Values); + } +} \ No newline at end of file diff --git a/src/Http/Authentication.Core/src/AuthenticationService.cs b/src/Http/Authentication.Core/src/AuthenticationService.cs new file mode 100644 index 0000000000000000000000000000000000000000..3e46df2f24aa4c88cbb24a5f458e5e407df6fcd3 --- /dev/null +++ b/src/Http/Authentication.Core/src/AuthenticationService.cs @@ -0,0 +1,303 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Implements <see cref="IAuthenticationService"/>. + /// </summary> + public class AuthenticationService : IAuthenticationService + { + /// <summary> + /// Constructor. + /// </summary> + /// <param name="schemes">The <see cref="IAuthenticationSchemeProvider"/>.</param> + /// <param name="handlers">The <see cref="IAuthenticationRequestHandler"/>.</param> + /// <param name="transform">The <see cref="IClaimsTransformation"/>.</param> + public AuthenticationService(IAuthenticationSchemeProvider schemes, IAuthenticationHandlerProvider handlers, IClaimsTransformation transform) + { + Schemes = schemes; + Handlers = handlers; + Transform = transform; + } + + /// <summary> + /// Used to lookup AuthenticationSchemes. + /// </summary> + public IAuthenticationSchemeProvider Schemes { get; } + + /// <summary> + /// Used to resolve IAuthenticationHandler instances. + /// </summary> + public IAuthenticationHandlerProvider Handlers { get; } + + /// <summary> + /// Used for claims transformation. + /// </summary> + public IClaimsTransformation Transform { get; } + + /// <summary> + /// Authenticate for the specified authentication scheme. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/>.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <returns>The result.</returns> + public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme) + { + if (scheme == null) + { + var defaultScheme = await Schemes.GetDefaultAuthenticateSchemeAsync(); + scheme = defaultScheme?.Name; + if (scheme == null) + { + throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found."); + } + } + + var handler = await Handlers.GetHandlerAsync(context, scheme); + if (handler == null) + { + throw await CreateMissingHandlerException(scheme); + } + + var result = await handler.AuthenticateAsync(); + if (result != null && result.Succeeded) + { + var transformed = await Transform.TransformAsync(result.Principal); + return AuthenticateResult.Success(new AuthenticationTicket(transformed, result.Properties, result.Ticket.AuthenticationScheme)); + } + return result; + } + + /// <summary> + /// Challenge the specified authentication scheme. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/>.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> + /// <returns>A task.</returns> + public virtual async Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + if (scheme == null) + { + var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync(); + scheme = defaultChallengeScheme?.Name; + if (scheme == null) + { + throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found."); + } + } + + var handler = await Handlers.GetHandlerAsync(context, scheme); + if (handler == null) + { + throw await CreateMissingHandlerException(scheme); + } + + await handler.ChallengeAsync(properties); + } + + /// <summary> + /// Forbid the specified authentication scheme. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/>.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> + /// <returns>A task.</returns> + public virtual async Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + if (scheme == null) + { + var defaultForbidScheme = await Schemes.GetDefaultForbidSchemeAsync(); + scheme = defaultForbidScheme?.Name; + if (scheme == null) + { + throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultForbidScheme found."); + } + } + + var handler = await Handlers.GetHandlerAsync(context, scheme); + if (handler == null) + { + throw await CreateMissingHandlerException(scheme); + } + + await handler.ForbidAsync(properties); + } + + /// <summary> + /// Sign a principal in for the specified authentication scheme. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/>.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="principal">The <see cref="ClaimsPrincipal"/> to sign in.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> + /// <returns>A task.</returns> + public virtual async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + if (scheme == null) + { + var defaultScheme = await Schemes.GetDefaultSignInSchemeAsync(); + scheme = defaultScheme?.Name; + if (scheme == null) + { + throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignInScheme found."); + } + } + + var handler = await Handlers.GetHandlerAsync(context, scheme); + if (handler == null) + { + throw await CreateMissingSignInHandlerException(scheme); + } + + var signInHandler = handler as IAuthenticationSignInHandler; + if (signInHandler == null) + { + throw await CreateMismatchedSignInHandlerException(scheme, handler); + } + + await signInHandler.SignInAsync(principal, properties); + } + + /// <summary> + /// Sign out the specified authentication scheme. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/>.</param> + /// <param name="scheme">The name of the authentication scheme.</param> + /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> + /// <returns>A task.</returns> + public virtual async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + if (scheme == null) + { + var defaultScheme = await Schemes.GetDefaultSignOutSchemeAsync(); + scheme = defaultScheme?.Name; + if (scheme == null) + { + throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignOutScheme found."); + } + } + + var handler = await Handlers.GetHandlerAsync(context, scheme); + if (handler == null) + { + throw await CreateMissingSignOutHandlerException(scheme); + } + + var signOutHandler = handler as IAuthenticationSignOutHandler; + if (signOutHandler == null) + { + throw await CreateMismatchedSignOutHandlerException(scheme, handler); + } + + await signOutHandler.SignOutAsync(properties); + } + + private async Task<Exception> CreateMissingHandlerException(string scheme) + { + var schemes = string.Join(", ", (await Schemes.GetAllSchemesAsync()).Select(sch => sch.Name)); + + var footer = $" Did you forget to call AddAuthentication().Add[SomeAuthHandler](\"{scheme}\",...)?"; + + if (string.IsNullOrEmpty(schemes)) + { + return new InvalidOperationException( + $"No authentication handlers are registered." + footer); + } + + return new InvalidOperationException( + $"No authentication handler is registered for the scheme '{scheme}'. The registered schemes are: {schemes}." + footer); + } + + private async Task<string> GetAllSignInSchemeNames() + { + return string.Join(", ", (await Schemes.GetAllSchemesAsync()) + .Where(sch => typeof(IAuthenticationSignInHandler).IsAssignableFrom(sch.HandlerType)) + .Select(sch => sch.Name)); + } + + private async Task<Exception> CreateMissingSignInHandlerException(string scheme) + { + var schemes = await GetAllSignInSchemeNames(); + + // CookieAuth is the only implementation of sign-in. + var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?"; + + if (string.IsNullOrEmpty(schemes)) + { + return new InvalidOperationException( + $"No sign-in authentication handlers are registered." + footer); + } + + return new InvalidOperationException( + $"No sign-in authentication handler is registered for the scheme '{scheme}'. The registered sign-in schemes are: {schemes}." + footer); + } + + private async Task<Exception> CreateMismatchedSignInHandlerException(string scheme, IAuthenticationHandler handler) + { + var schemes = await GetAllSignInSchemeNames(); + + var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for SignInAsync. "; + + if (string.IsNullOrEmpty(schemes)) + { + // CookieAuth is the only implementation of sign-in. + return new InvalidOperationException(mismatchError + + $"Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and SignInAsync(\"Cookies\",...)?"); + } + + return new InvalidOperationException(mismatchError + $"The registered sign-in schemes are: {schemes}."); + } + + private async Task<string> GetAllSignOutSchemeNames() + { + return string.Join(", ", (await Schemes.GetAllSchemesAsync()) + .Where(sch => typeof(IAuthenticationSignOutHandler).IsAssignableFrom(sch.HandlerType)) + .Select(sch => sch.Name)); + } + + private async Task<Exception> CreateMissingSignOutHandlerException(string scheme) + { + var schemes = await GetAllSignOutSchemeNames(); + + var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?"; + + if (string.IsNullOrEmpty(schemes)) + { + // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it. + return new InvalidOperationException($"No sign-out authentication handlers are registered." + footer); + } + + return new InvalidOperationException( + $"No sign-out authentication handler is registered for the scheme '{scheme}'. The registered sign-out schemes are: {schemes}." + footer); + } + + private async Task<Exception> CreateMismatchedSignOutHandlerException(string scheme, IAuthenticationHandler handler) + { + var schemes = await GetAllSignOutSchemeNames(); + + var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for {nameof(SignOutAsync)}. "; + + if (string.IsNullOrEmpty(schemes)) + { + // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it. + return new InvalidOperationException(mismatchError + + $"Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and {nameof(SignOutAsync)}(\"Cookies\",...)?"); + } + + return new InvalidOperationException(mismatchError + $"The registered sign-out schemes are: {schemes}."); + } + } +} diff --git a/src/Http/Authentication.Core/src/Microsoft.AspNetCore.Authentication.Core.csproj b/src/Http/Authentication.Core/src/Microsoft.AspNetCore.Authentication.Core.csproj new file mode 100644 index 0000000000000000000000000000000000000000..c10bfb3656af825d39e106d6ab6678c7d9c4f14d --- /dev/null +++ b/src/Http/Authentication.Core/src/Microsoft.AspNetCore.Authentication.Core.csproj @@ -0,0 +1,18 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>ASP.NET Core common types used by the various authentication middleware components.</Description> + <TargetFramework>netstandard2.0</TargetFramework> + <NoWarn>$(NoWarn);CS1591</NoWarn> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore;authentication;security</PackageTags> + <EnableApiCheck>false</EnableApiCheck> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Authentication.Abstractions" /> + <Reference Include="Microsoft.AspNetCore.Http" /> + <Reference Include="Microsoft.AspNetCore.Http.Extensions" /> + </ItemGroup> + +</Project> diff --git a/src/Http/Authentication.Core/src/NoopClaimsTransformation.cs b/src/Http/Authentication.Core/src/NoopClaimsTransformation.cs new file mode 100644 index 0000000000000000000000000000000000000000..83c488fe421b2fd73e85e1ab50f2d121507454f7 --- /dev/null +++ b/src/Http/Authentication.Core/src/NoopClaimsTransformation.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication +{ + /// <summary> + /// Default claims transformation is a no-op. + /// </summary> + public class NoopClaimsTransformation : IClaimsTransformation + { + /// <summary> + /// Returns the principal unchanged. + /// </summary> + /// <param name="principal">The user.</param> + /// <returns>The principal unchanged.</returns> + public virtual Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) + { + return Task.FromResult(principal); + } + } +} diff --git a/src/Http/Authentication.Core/src/baseline.netcore.json b/src/Http/Authentication.Core/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..62aeb4473802c2a8fc4304687b439971f256c307 --- /dev/null +++ b/src/Http/Authentication.Core/src/baseline.netcore.json @@ -0,0 +1,515 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Core, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_OriginalPathBase", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OriginalPathBase", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OriginalPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OriginalPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationHandlerProvider", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Schemes", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHandlerAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.IAuthenticationHandler>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "schemes", + "Type": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetDefaultAuthenticateSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultChallengeSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultForbidSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultSignInSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultSignOutSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetSchemeAsync", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetRequestHandlerSchemesAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authentication.AuthenticationScheme>>", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RemoveScheme", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetAllSchemesAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authentication.AuthenticationScheme>>", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Authentication.AuthenticationOptions>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationService", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationService" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Schemes", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Handlers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Transform", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.IClaimsTransformation", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationService", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationService", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationService", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationService", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationService", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "schemes", + "Type": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider" + }, + { + "Name": "handlers", + "Type": "Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider" + }, + { + "Name": "transform", + "Type": "Microsoft.AspNetCore.Authentication.IClaimsTransformation" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.NoopClaimsTransformation", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IClaimsTransformation" + ], + "Members": [ + { + "Kind": "Method", + "Name": "TransformAsync", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Security.Claims.ClaimsPrincipal>", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IClaimsTransformation", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.DependencyInjection.AuthenticationCoreServiceCollectionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddAuthenticationCore", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddAuthenticationCore", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "configureOptions", + "Type": "System.Action<Microsoft.AspNetCore.Authentication.AuthenticationOptions>" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs b/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..639c9b558ea633576abc6b0db8b3b7d0c5b70112 --- /dev/null +++ b/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Core.Test +{ + public class AuthenticationPropertiesTests + { + [Fact] + public void DefaultConstructor_EmptyCollections() + { + var props = new AuthenticationProperties(); + Assert.Empty(props.Items); + Assert.Empty(props.Parameters); + } + + [Fact] + public void ItemsConstructor_ReusesItemsDictionary() + { + var items = new Dictionary<string, string> + { + ["foo"] = "bar", + }; + var props = new AuthenticationProperties(items); + Assert.Same(items, props.Items); + Assert.Empty(props.Parameters); + } + + [Fact] + public void FullConstructor_ReusesDictionaries() + { + var items = new Dictionary<string, string> + { + ["foo"] = "bar", + }; + var parameters = new Dictionary<string, object> + { + ["number"] = 1234, + ["list"] = new List<string> { "a", "b", "c" }, + }; + var props = new AuthenticationProperties(items, parameters); + Assert.Same(items, props.Items); + Assert.Same(parameters, props.Parameters); + } + + [Fact] + public void GetSetString() + { + var props = new AuthenticationProperties(); + Assert.Null(props.GetString("foo")); + Assert.Equal(0, props.Items.Count); + + props.SetString("foo", "foo bar"); + Assert.Equal("foo bar", props.GetString("foo")); + Assert.Equal("foo bar", props.Items["foo"]); + Assert.Equal(1, props.Items.Count); + + props.SetString("foo", "foo baz"); + Assert.Equal("foo baz", props.GetString("foo")); + Assert.Equal("foo baz", props.Items["foo"]); + Assert.Equal(1, props.Items.Count); + + props.SetString("bar", "xy"); + Assert.Equal("xy", props.GetString("bar")); + Assert.Equal("xy", props.Items["bar"]); + Assert.Equal(2, props.Items.Count); + + props.SetString("bar", string.Empty); + Assert.Equal(string.Empty, props.GetString("bar")); + Assert.Equal(string.Empty, props.Items["bar"]); + + props.SetString("foo", null); + Assert.Null(props.GetString("foo")); + Assert.Equal(1, props.Items.Count); + } + + [Fact] + public void GetSetParameter_String() + { + var props = new AuthenticationProperties(); + Assert.Null(props.GetParameter<string>("foo")); + Assert.Equal(0, props.Parameters.Count); + + props.SetParameter<string>("foo", "foo bar"); + Assert.Equal("foo bar", props.GetParameter<string>("foo")); + Assert.Equal("foo bar", props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + + props.SetParameter<string>("foo", null); + Assert.Null(props.GetParameter<string>("foo")); + Assert.Null(props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + } + + [Fact] + public void GetSetParameter_Int() + { + var props = new AuthenticationProperties(); + Assert.Null(props.GetParameter<int?>("foo")); + Assert.Equal(0, props.Parameters.Count); + + props.SetParameter<int?>("foo", 123); + Assert.Equal(123, props.GetParameter<int?>("foo")); + Assert.Equal(123, props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + + props.SetParameter<int?>("foo", null); + Assert.Null(props.GetParameter<int?>("foo")); + Assert.Null(props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + } + + [Fact] + public void GetSetParameter_Collection() + { + var props = new AuthenticationProperties(); + Assert.Null(props.GetParameter<int?>("foo")); + Assert.Equal(0, props.Parameters.Count); + + var list = new string[] { "a", "b", "c" }; + props.SetParameter<ICollection<string>>("foo", list); + Assert.Equal(new string[] { "a", "b", "c" }, props.GetParameter<ICollection<string>>("foo")); + Assert.Same(list, props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + + props.SetParameter<ICollection<string>>("foo", null); + Assert.Null(props.GetParameter<ICollection<string>>("foo")); + Assert.Null(props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + } + + [Fact] + public void IsPersistent_Test() + { + var props = new AuthenticationProperties(); + Assert.False(props.IsPersistent); + + props.IsPersistent = true; + Assert.True(props.IsPersistent); + Assert.Equal(string.Empty, props.Items.First().Value); + + props.Items.Clear(); + Assert.False(props.IsPersistent); + } + + [Fact] + public void RedirectUri_Test() + { + var props = new AuthenticationProperties(); + Assert.Null(props.RedirectUri); + + props.RedirectUri = "http://example.com"; + Assert.Equal("http://example.com", props.RedirectUri); + Assert.Equal("http://example.com", props.Items.First().Value); + + props.Items.Clear(); + Assert.Null(props.RedirectUri); + } + + [Fact] + public void IssuedUtc_Test() + { + var props = new AuthenticationProperties(); + Assert.Null(props.IssuedUtc); + + props.IssuedUtc = new DateTimeOffset(new DateTime(2018, 03, 21, 0, 0, 0, DateTimeKind.Utc)); + Assert.Equal(new DateTimeOffset(new DateTime(2018, 03, 21, 0, 0, 0, DateTimeKind.Utc)), props.IssuedUtc); + Assert.Equal("Wed, 21 Mar 2018 00:00:00 GMT", props.Items.First().Value); + + props.Items.Clear(); + Assert.Null(props.IssuedUtc); + } + + [Fact] + public void ExpiresUtc_Test() + { + var props = new AuthenticationProperties(); + Assert.Null(props.ExpiresUtc); + + props.ExpiresUtc = new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc)); + Assert.Equal(new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc)), props.ExpiresUtc); + Assert.Equal("Mon, 19 Mar 2018 12:34:56 GMT", props.Items.First().Value); + + props.Items.Clear(); + Assert.Null(props.ExpiresUtc); + } + + [Fact] + public void AllowRefresh_Test() + { + var props = new AuthenticationProperties(); + Assert.Null(props.AllowRefresh); + + props.AllowRefresh = true; + Assert.True(props.AllowRefresh); + Assert.Equal("True", props.Items.First().Value); + + props.AllowRefresh = false; + Assert.False(props.AllowRefresh); + Assert.Equal("False", props.Items.First().Value); + + props.Items.Clear(); + Assert.Null(props.AllowRefresh); + } + } +} diff --git a/src/Http/Authentication.Core/test/AuthenticationSchemeProviderTests.cs b/src/Http/Authentication.Core/test/AuthenticationSchemeProviderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..82602000aa3730cb592f7aa67af0e5c81f5d9585 --- /dev/null +++ b/src/Http/Authentication.Core/test/AuthenticationSchemeProviderTests.cs @@ -0,0 +1,207 @@ + +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public class AuthenticationSchemeProviderTests + { + [Fact] + public async Task NoDefaultsByDefault() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<SignInHandler>("B", "whatever"); + }).BuildServiceProvider(); + + var provider = services.GetRequiredService<IAuthenticationSchemeProvider>(); + Assert.Null(await provider.GetDefaultForbidSchemeAsync()); + Assert.Null(await provider.GetDefaultAuthenticateSchemeAsync()); + Assert.Null(await provider.GetDefaultChallengeSchemeAsync()); + Assert.Null(await provider.GetDefaultSignInSchemeAsync()); + Assert.Null(await provider.GetDefaultSignOutSchemeAsync()); + } + + [Fact] + public async Task DefaultSchemesFallbackToDefaultScheme() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.DefaultScheme = "B"; + o.AddScheme<SignInHandler>("B", "whatever"); + }).BuildServiceProvider(); + + var provider = services.GetRequiredService<IAuthenticationSchemeProvider>(); + Assert.Equal("B", (await provider.GetDefaultForbidSchemeAsync()).Name); + Assert.Equal("B", (await provider.GetDefaultAuthenticateSchemeAsync()).Name); + Assert.Equal("B", (await provider.GetDefaultChallengeSchemeAsync()).Name); + Assert.Equal("B", (await provider.GetDefaultSignInSchemeAsync()).Name); + Assert.Equal("B", (await provider.GetDefaultSignOutSchemeAsync()).Name); + } + + + [Fact] + public async Task DefaultSignOutFallsbackToSignIn() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<SignInHandler>("signin", "whatever"); + o.AddScheme<Handler>("foobly", "whatever"); + o.DefaultSignInScheme = "signin"; + }).BuildServiceProvider(); + + var provider = services.GetRequiredService<IAuthenticationSchemeProvider>(); + var scheme = await provider.GetDefaultSignOutSchemeAsync(); + Assert.NotNull(scheme); + Assert.Equal("signin", scheme.Name); + } + + [Fact] + public async Task DefaultForbidFallsbackToChallenge() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<Handler>("challenge", "whatever"); + o.AddScheme<Handler>("foobly", "whatever"); + o.DefaultChallengeScheme = "challenge"; + }).BuildServiceProvider(); + + var provider = services.GetRequiredService<IAuthenticationSchemeProvider>(); + var scheme = await provider.GetDefaultForbidSchemeAsync(); + Assert.NotNull(scheme); + Assert.Equal("challenge", scheme.Name); + } + + [Fact] + public async Task DefaultSchemesAreSet() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<SignInHandler>("A", "whatever"); + o.AddScheme<SignInHandler>("B", "whatever"); + o.AddScheme<SignInHandler>("C", "whatever"); + o.AddScheme<SignInHandler>("Def", "whatever"); + o.DefaultScheme = "Def"; + o.DefaultChallengeScheme = "A"; + o.DefaultForbidScheme = "B"; + o.DefaultSignInScheme = "C"; + o.DefaultSignOutScheme = "A"; + o.DefaultAuthenticateScheme = "C"; + }).BuildServiceProvider(); + + var provider = services.GetRequiredService<IAuthenticationSchemeProvider>(); + Assert.Equal("B", (await provider.GetDefaultForbidSchemeAsync()).Name); + Assert.Equal("C", (await provider.GetDefaultAuthenticateSchemeAsync()).Name); + Assert.Equal("A", (await provider.GetDefaultChallengeSchemeAsync()).Name); + Assert.Equal("C", (await provider.GetDefaultSignInSchemeAsync()).Name); + Assert.Equal("A", (await provider.GetDefaultSignOutSchemeAsync()).Name); + } + + [Fact] + public async Task SignOutWillDefaultsToSignInThatDoesNotSignOut() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<Handler>("signin", "whatever"); + o.DefaultSignInScheme = "signin"; + }).BuildServiceProvider(); + + var provider = services.GetRequiredService<IAuthenticationSchemeProvider>(); + Assert.NotNull(await provider.GetDefaultSignOutSchemeAsync()); + } + + [Fact] + public void SchemeRegistrationIsCaseSensitive() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<Handler>("signin", "whatever"); + o.AddScheme<Handler>("signin", "whatever"); + }).BuildServiceProvider(); + + var error = Assert.Throws<InvalidOperationException>(() => services.GetRequiredService<IAuthenticationSchemeProvider>()); + + Assert.Contains("Scheme already exists: signin", error.Message); + } + + [Fact] + public async Task LookupUsesProvidedStringComparer() + { + var services = new ServiceCollection().AddOptions() + .AddSingleton<IAuthenticationSchemeProvider, IgnoreCaseSchemeProvider>() + .AddAuthenticationCore(o => o.AddScheme<Handler>("signin", "whatever")) + .BuildServiceProvider(); + + var provider = services.GetRequiredService<IAuthenticationSchemeProvider>(); + + var a = await provider.GetSchemeAsync("signin"); + var b = await provider.GetSchemeAsync("SignIn"); + var c = await provider.GetSchemeAsync("SIGNIN"); + + Assert.NotNull(a); + Assert.Same(a, b); + Assert.Same(b, c); + } + + private class Handler : IAuthenticationHandler + { + public Task<AuthenticateResult> AuthenticateAsync() + { + throw new NotImplementedException(); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + throw new NotImplementedException(); + } + } + + private class SignInHandler : Handler, IAuthenticationSignInHandler + { + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + } + + private class SignOutHandler : Handler, IAuthenticationSignOutHandler + { + public Task SignOutAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + } + + private class IgnoreCaseSchemeProvider : AuthenticationSchemeProvider + { + public IgnoreCaseSchemeProvider(IOptions<AuthenticationOptions> options) + : base(options, new Dictionary<string, AuthenticationScheme>(StringComparer.OrdinalIgnoreCase)) + { + } + } + } +} diff --git a/src/Http/Authentication.Core/test/AuthenticationServiceTests.cs b/src/Http/Authentication.Core/test/AuthenticationServiceTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..e21ea40d518ba720c756f46d3b9c798cfdc1983d --- /dev/null +++ b/src/Http/Authentication.Core/test/AuthenticationServiceTests.cs @@ -0,0 +1,355 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public class AuthenticationServiceTests + { + [Fact] + public async Task AuthenticateThrowsForSchemeMismatch() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<BaseHandler>("base", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.AuthenticateAsync("base"); + var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.AuthenticateAsync("missing")); + Assert.Contains("base", ex.Message); + } + + [Fact] + public async Task ChallengeThrowsForSchemeMismatch() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<BaseHandler>("base", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.ChallengeAsync("base"); + var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.ChallengeAsync("missing")); + Assert.Contains("base", ex.Message); + } + + [Fact] + public async Task ForbidThrowsForSchemeMismatch() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<BaseHandler>("base", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.ForbidAsync("base"); + var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.ForbidAsync("missing")); + Assert.Contains("base", ex.Message); + } + + [Fact] + public async Task CanOnlySignInIfSupported() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<UberHandler>("uber", "whatever"); + o.AddScheme<BaseHandler>("base", "whatever"); + o.AddScheme<SignInHandler>("signin", "whatever"); + o.AddScheme<SignOutHandler>("signout", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.SignInAsync("uber", new ClaimsPrincipal(), null); + var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync("base", new ClaimsPrincipal(), null)); + Assert.Contains("uber", ex.Message); + Assert.Contains("signin", ex.Message); + await context.SignInAsync("signin", new ClaimsPrincipal(), null); + ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync("signout", new ClaimsPrincipal(), null)); + Assert.Contains("uber", ex.Message); + Assert.Contains("signin", ex.Message); + } + + [Fact] + public async Task CanOnlySignOutIfSupported() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<UberHandler>("uber", "whatever"); + o.AddScheme<BaseHandler>("base", "whatever"); + o.AddScheme<SignInHandler>("signin", "whatever"); + o.AddScheme<SignOutHandler>("signout", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.SignOutAsync("uber"); + var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync("base")); + Assert.Contains("uber", ex.Message); + Assert.Contains("signout", ex.Message); + await context.SignOutAsync("signout"); + await context.SignOutAsync("signin"); + } + + [Fact] + public async Task ServicesWithDefaultIAuthenticationHandlerMethodsTest() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<BaseHandler>("base", "whatever"); + o.DefaultScheme = "base"; + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.AuthenticateAsync(); + await context.ChallengeAsync(); + await context.ForbidAsync(); + var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync()); + Assert.Contains("cannot be used for SignOutAsync", ex.Message); + ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal())); + Assert.Contains("cannot be used for SignInAsync", ex.Message); + } + + [Fact] + public async Task ServicesWithDefaultUberMethodsTest() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<UberHandler>("base", "whatever"); + o.DefaultScheme = "base"; + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.AuthenticateAsync(); + await context.ChallengeAsync(); + await context.ForbidAsync(); + await context.SignOutAsync(); + await context.SignInAsync(new ClaimsPrincipal()); + } + + [Fact] + public async Task ServicesWithDefaultSignInMethodsTest() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<SignInHandler>("base", "whatever"); + o.DefaultScheme = "base"; + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.AuthenticateAsync(); + await context.ChallengeAsync(); + await context.ForbidAsync(); + await context.SignOutAsync(); + await context.SignInAsync(new ClaimsPrincipal()); + } + + [Fact] + public async Task ServicesWithDefaultSignOutMethodsTest() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<SignOutHandler>("base", "whatever"); + o.DefaultScheme = "base"; + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.AuthenticateAsync(); + await context.ChallengeAsync(); + await context.ForbidAsync(); + await context.SignOutAsync(); + var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal())); + Assert.Contains("cannot be used for SignInAsync", ex.Message); + } + + [Fact] + public async Task ServicesWithDefaultForbidMethod_CallsForbidMethod() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme<ForbidHandler>("forbid", "whatever"); + o.DefaultForbidScheme = "forbid"; + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.ForbidAsync(); + } + + + private class BaseHandler : IAuthenticationHandler + { + public Task<AuthenticateResult> AuthenticateAsync() + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + return Task.FromResult(0); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + return Task.FromResult(0); + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + return Task.FromResult(0); + } + } + + private class SignInHandler : IAuthenticationSignInHandler + { + public Task<AuthenticateResult> AuthenticateAsync() + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + return Task.FromResult(0); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + return Task.FromResult(0); + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + return Task.FromResult(0); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + return Task.FromResult(0); + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + return Task.FromResult(0); + } + } + + public class SignOutHandler : IAuthenticationSignOutHandler + { + public Task<AuthenticateResult> AuthenticateAsync() + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + return Task.FromResult(0); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + return Task.FromResult(0); + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + return Task.FromResult(0); + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + return Task.FromResult(0); + } + } + + private class UberHandler : IAuthenticationHandler, IAuthenticationRequestHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler + { + public Task<AuthenticateResult> AuthenticateAsync() + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + return Task.FromResult(0); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + return Task.FromResult(0); + } + + public Task<bool> HandleRequestAsync() + { + return Task.FromResult(false); + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + return Task.FromResult(0); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + return Task.FromResult(0); + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + return Task.FromResult(0); + } + } + + private class ForbidHandler : IAuthenticationHandler, IAuthenticationRequestHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler + { + public Task<AuthenticateResult> AuthenticateAsync() + { + throw new NotImplementedException(); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + return Task.FromResult(0); + } + + public Task<bool> HandleRequestAsync() + { + throw new NotImplementedException(); + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + return Task.FromResult(0); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + } + + } +} diff --git a/src/Http/Authentication.Core/test/Microsoft.AspNetCore.Authentication.Core.Test.csproj b/src/Http/Authentication.Core/test/Microsoft.AspNetCore.Authentication.Core.Test.csproj new file mode 100644 index 0000000000000000000000000000000000000000..48197031971f8ef6cd94fdad9e41bf4392c79f6e --- /dev/null +++ b/src/Http/Authentication.Core/test/Microsoft.AspNetCore.Authentication.Core.Test.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Authentication.Core" /> + <Reference Include="Microsoft.Extensions.DependencyInjection" /> + </ItemGroup> + +</Project> diff --git a/src/Http/Authentication.Core/test/TokenExtensionTests.cs b/src/Http/Authentication.Core/test/TokenExtensionTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..7215d526e9dbfa1067c43f540094624c6dd3c32a --- /dev/null +++ b/src/Http/Authentication.Core/test/TokenExtensionTests.cs @@ -0,0 +1,200 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public class TokenExtensionTests + { + [Fact] + public void CanStoreMultipleTokens() + { + var props = new AuthenticationProperties(); + var tokens = new List<AuthenticationToken>(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + Assert.Equal("1", props.GetTokenValue("One")); + Assert.Equal("2", props.GetTokenValue("Two")); + Assert.Equal("3", props.GetTokenValue("Three")); + Assert.Equal(3, props.GetTokens().Count()); + } + + [Fact] + public void SubsequentStoreTokenDeletesPreviousTokens() + { + var props = new AuthenticationProperties(); + var tokens = new List<AuthenticationToken>(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + + props.StoreTokens(tokens); + + props.StoreTokens(new[] { new AuthenticationToken { Name = "Zero", Value = "0" } }); + + Assert.Equal("0", props.GetTokenValue("Zero")); + Assert.Null(props.GetTokenValue("One")); + Assert.Null(props.GetTokenValue("Two")); + Assert.Null(props.GetTokenValue("Three")); + Assert.Single(props.GetTokens()); + } + + [Fact] + public void CanUpdateTokens() + { + var props = new AuthenticationProperties(); + var tokens = new List<AuthenticationToken>(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + tok1.Value = ".1"; + tok2.Value = ".2"; + tok3.Value = ".3"; + props.StoreTokens(tokens); + + Assert.Equal(".1", props.GetTokenValue("One")); + Assert.Equal(".2", props.GetTokenValue("Two")); + Assert.Equal(".3", props.GetTokenValue("Three")); + Assert.Equal(3, props.GetTokens().Count()); + } + + [Fact] + public void CanUpdateTokenValues() + { + var props = new AuthenticationProperties(); + var tokens = new List<AuthenticationToken>(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + Assert.True(props.UpdateTokenValue("One", ".11")); + Assert.True(props.UpdateTokenValue("Two", ".22")); + Assert.True(props.UpdateTokenValue("Three", ".33")); + + Assert.Equal(".11", props.GetTokenValue("One")); + Assert.Equal(".22", props.GetTokenValue("Two")); + Assert.Equal(".33", props.GetTokenValue("Three")); + Assert.Equal(3, props.GetTokens().Count()); + } + + [Fact] + public void UpdateTokenValueReturnsFalseForUnknownToken() + { + var props = new AuthenticationProperties(); + var tokens = new List<AuthenticationToken>(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + Assert.False(props.UpdateTokenValue("ONE", ".11")); + Assert.False(props.UpdateTokenValue("Jigglypuff", ".11")); + + Assert.Null(props.GetTokenValue("ONE")); + Assert.Null(props.GetTokenValue("Jigglypuff")); + Assert.Equal(3, props.GetTokens().Count()); + } + + [Fact] + public async Task GetTokenWorksWithDefaultAuthenticateScheme() + { + var context = new DefaultHttpContext(); + var services = new ServiceCollection().AddOptions() + .AddAuthenticationCore(o => + { + o.DefaultScheme = "simple"; + o.AddScheme("simple", s => s.HandlerType = typeof(SimpleAuth)); + }); + context.RequestServices = services.BuildServiceProvider(); + + Assert.Equal("1", await context.GetTokenAsync("One")); + Assert.Equal("2", await context.GetTokenAsync("Two")); + Assert.Equal("3", await context.GetTokenAsync("Three")); + } + + [Fact] + public async Task GetTokenWorksWithExplicitScheme() + { + var context = new DefaultHttpContext(); + var services = new ServiceCollection().AddOptions() + .AddAuthenticationCore(o => o.AddScheme("simple", s => s.HandlerType = typeof(SimpleAuth))); + context.RequestServices = services.BuildServiceProvider(); + + Assert.Equal("1", await context.GetTokenAsync("simple", "One")); + Assert.Equal("2", await context.GetTokenAsync("simple", "Two")); + Assert.Equal("3", await context.GetTokenAsync("simple", "Three")); + } + + private class SimpleAuth : IAuthenticationHandler + { + public Task<AuthenticateResult> AuthenticateAsync() + { + var props = new AuthenticationProperties(); + var tokens = new List<AuthenticationToken>(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), props, "simple"))); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + return Task.FromResult(0); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + } + + } +} diff --git a/src/Http/Headers/src/BaseHeaderParser.cs b/src/Http/Headers/src/BaseHeaderParser.cs new file mode 100644 index 0000000000000000000000000000000000000000..f3caaafb706cd198897903771eeebaceeca3091a --- /dev/null +++ b/src/Http/Headers/src/BaseHeaderParser.cs @@ -0,0 +1,72 @@ +// 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.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + internal abstract class BaseHeaderParser<T> : HttpHeaderParser<T> + { + protected BaseHeaderParser(bool supportsMultipleValues) + : base(supportsMultipleValues) + { + } + + protected abstract int GetParsedValueLength(StringSegment value, int startIndex, out T parsedValue); + + public sealed override bool TryParseValue(StringSegment value, ref int index, out T parsedValue) + { + parsedValue = default(T); + + // If multiple values are supported (i.e. list of values), then accept an empty string: The header may + // be added multiple times to the request/response message. E.g. + // Accept: text/xml; q=1 + // Accept: + // Accept: text/plain; q=0.2 + if (StringSegment.IsNullOrEmpty(value) || (index == value.Length)) + { + return SupportsMultipleValues; + } + + var separatorFound = false; + var current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(value, index, SupportsMultipleValues, + out separatorFound); + + if (separatorFound && !SupportsMultipleValues) + { + return false; // leading separators not allowed if we don't support multiple values. + } + + if (current == value.Length) + { + if (SupportsMultipleValues) + { + index = current; + } + return SupportsMultipleValues; + } + + T result; + var length = GetParsedValueLength(value, current, out result); + + if (length == 0) + { + return false; + } + + current = current + length; + current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(value, current, SupportsMultipleValues, + out separatorFound); + + // If we support multiple values and we've not reached the end of the string, then we must have a separator. + if ((separatorFound && !SupportsMultipleValues) || (!separatorFound && (current < value.Length))) + { + return false; + } + + index = current; + parsedValue = result; + return true; + } + } +} diff --git a/src/Http/Headers/src/CacheControlHeaderValue.cs b/src/Http/Headers/src/CacheControlHeaderValue.cs new file mode 100644 index 0000000000000000000000000000000000000000..81e18faf473efad6182d104c57c95232f733afa8 --- /dev/null +++ b/src/Http/Headers/src/CacheControlHeaderValue.cs @@ -0,0 +1,664 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Text; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + public class CacheControlHeaderValue + { + public static readonly string PublicString = "public"; + public static readonly string PrivateString = "private"; + public static readonly string MaxAgeString = "max-age"; + public static readonly string SharedMaxAgeString = "s-maxage"; + public static readonly string NoCacheString = "no-cache"; + public static readonly string NoStoreString = "no-store"; + public static readonly string MaxStaleString = "max-stale"; + public static readonly string MinFreshString = "min-fresh"; + public static readonly string NoTransformString = "no-transform"; + public static readonly string OnlyIfCachedString = "only-if-cached"; + public static readonly string MustRevalidateString = "must-revalidate"; + public static readonly string ProxyRevalidateString = "proxy-revalidate"; + + // The Cache-Control header is special: It is a header supporting a list of values, but we represent the list + // as _one_ instance of CacheControlHeaderValue. I.e we set 'SupportsMultipleValues' to 'true' since it is + // OK to have multiple Cache-Control headers in a request/response message. However, after parsing all + // Cache-Control headers, only one instance of CacheControlHeaderValue is created (if all headers contain valid + // values, otherwise we may have multiple strings containing the invalid values). + private static readonly HttpHeaderParser<CacheControlHeaderValue> Parser + = new GenericHeaderParser<CacheControlHeaderValue>(true, GetCacheControlLength); + + private static readonly Action<StringSegment> CheckIsValidTokenAction = CheckIsValidToken; + + private bool _noCache; + private ICollection<StringSegment> _noCacheHeaders; + private bool _noStore; + private TimeSpan? _maxAge; + private TimeSpan? _sharedMaxAge; + private bool _maxStale; + private TimeSpan? _maxStaleLimit; + private TimeSpan? _minFresh; + private bool _noTransform; + private bool _onlyIfCached; + private bool _public; + private bool _private; + private ICollection<StringSegment> _privateHeaders; + private bool _mustRevalidate; + private bool _proxyRevalidate; + private IList<NameValueHeaderValue> _extensions; + + public CacheControlHeaderValue() + { + // This type is unique in that there is no single required parameter. + } + + public bool NoCache + { + get { return _noCache; } + set { _noCache = value; } + } + + public ICollection<StringSegment> NoCacheHeaders + { + get + { + if (_noCacheHeaders == null) + { + _noCacheHeaders = new ObjectCollection<StringSegment>(CheckIsValidTokenAction); + } + return _noCacheHeaders; + } + } + + public bool NoStore + { + get { return _noStore; } + set { _noStore = value; } + } + + public TimeSpan? MaxAge + { + get { return _maxAge; } + set { _maxAge = value; } + } + + public TimeSpan? SharedMaxAge + { + get { return _sharedMaxAge; } + set { _sharedMaxAge = value; } + } + + public bool MaxStale + { + get { return _maxStale; } + set { _maxStale = value; } + } + + public TimeSpan? MaxStaleLimit + { + get { return _maxStaleLimit; } + set { _maxStaleLimit = value; } + } + + public TimeSpan? MinFresh + { + get { return _minFresh; } + set { _minFresh = value; } + } + + public bool NoTransform + { + get { return _noTransform; } + set { _noTransform = value; } + } + + public bool OnlyIfCached + { + get { return _onlyIfCached; } + set { _onlyIfCached = value; } + } + + public bool Public + { + get { return _public; } + set { _public = value; } + } + + public bool Private + { + get { return _private; } + set { _private = value; } + } + + public ICollection<StringSegment> PrivateHeaders + { + get + { + if (_privateHeaders == null) + { + _privateHeaders = new ObjectCollection<StringSegment>(CheckIsValidTokenAction); + } + return _privateHeaders; + } + } + + public bool MustRevalidate + { + get { return _mustRevalidate; } + set { _mustRevalidate = value; } + } + + public bool ProxyRevalidate + { + get { return _proxyRevalidate; } + set { _proxyRevalidate = value; } + } + + public IList<NameValueHeaderValue> Extensions + { + get + { + if (_extensions == null) + { + _extensions = new ObjectCollection<NameValueHeaderValue>(); + } + return _extensions; + } + } + + public override string ToString() + { + var sb = new StringBuilder(); + + AppendValueIfRequired(sb, _noStore, NoStoreString); + AppendValueIfRequired(sb, _noTransform, NoTransformString); + AppendValueIfRequired(sb, _onlyIfCached, OnlyIfCachedString); + AppendValueIfRequired(sb, _public, PublicString); + AppendValueIfRequired(sb, _mustRevalidate, MustRevalidateString); + AppendValueIfRequired(sb, _proxyRevalidate, ProxyRevalidateString); + + if (_noCache) + { + AppendValueWithSeparatorIfRequired(sb, NoCacheString); + if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0)) + { + sb.Append("=\""); + AppendValues(sb, _noCacheHeaders); + sb.Append('\"'); + } + } + + if (_maxAge.HasValue) + { + AppendValueWithSeparatorIfRequired(sb, MaxAgeString); + sb.Append('='); + sb.Append(((int)_maxAge.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + } + + if (_sharedMaxAge.HasValue) + { + AppendValueWithSeparatorIfRequired(sb, SharedMaxAgeString); + sb.Append('='); + sb.Append(((int)_sharedMaxAge.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + } + + if (_maxStale) + { + AppendValueWithSeparatorIfRequired(sb, MaxStaleString); + if (_maxStaleLimit.HasValue) + { + sb.Append('='); + sb.Append(((int)_maxStaleLimit.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + } + } + + if (_minFresh.HasValue) + { + AppendValueWithSeparatorIfRequired(sb, MinFreshString); + sb.Append('='); + sb.Append(((int)_minFresh.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + } + + if (_private) + { + AppendValueWithSeparatorIfRequired(sb, PrivateString); + if ((_privateHeaders != null) && (_privateHeaders.Count > 0)) + { + sb.Append("=\""); + AppendValues(sb, _privateHeaders); + sb.Append('\"'); + } + } + + NameValueHeaderValue.ToString(_extensions, ',', false, sb); + + return sb.ToString(); + } + + public override bool Equals(object obj) + { + var other = obj as CacheControlHeaderValue; + + if (other == null) + { + return false; + } + + if ((_noCache != other._noCache) || (_noStore != other._noStore) || (_maxAge != other._maxAge) || + (_sharedMaxAge != other._sharedMaxAge) || (_maxStale != other._maxStale) || + (_maxStaleLimit != other._maxStaleLimit) || (_minFresh != other._minFresh) || + (_noTransform != other._noTransform) || (_onlyIfCached != other._onlyIfCached) || + (_public != other._public) || (_private != other._private) || + (_mustRevalidate != other._mustRevalidate) || (_proxyRevalidate != other._proxyRevalidate)) + { + return false; + } + + if (!HeaderUtilities.AreEqualCollections(_noCacheHeaders, other._noCacheHeaders, + StringSegmentComparer.OrdinalIgnoreCase)) + { + return false; + } + + if (!HeaderUtilities.AreEqualCollections(_privateHeaders, other._privateHeaders, + StringSegmentComparer.OrdinalIgnoreCase)) + { + return false; + } + + if (!HeaderUtilities.AreEqualCollections(_extensions, other._extensions)) + { + return false; + } + + return true; + } + + public override int GetHashCode() + { + // Use a different bit for bool fields: bool.GetHashCode() will return 0 (false) or 1 (true). So we would + // end up having the same hash code for e.g. two instances where one has only noCache set and the other + // only noStore. + int result = _noCache.GetHashCode() ^ (_noStore.GetHashCode() << 1) ^ (_maxStale.GetHashCode() << 2) ^ + (_noTransform.GetHashCode() << 3) ^ (_onlyIfCached.GetHashCode() << 4) ^ + (_public.GetHashCode() << 5) ^ (_private.GetHashCode() << 6) ^ + (_mustRevalidate.GetHashCode() << 7) ^ (_proxyRevalidate.GetHashCode() << 8); + + // XOR the hashcode of timespan values with different numbers to make sure two instances with the same + // timespan set on different fields result in different hashcodes. + result = result ^ (_maxAge.HasValue ? _maxAge.Value.GetHashCode() ^ 1 : 0) ^ + (_sharedMaxAge.HasValue ? _sharedMaxAge.Value.GetHashCode() ^ 2 : 0) ^ + (_maxStaleLimit.HasValue ? _maxStaleLimit.Value.GetHashCode() ^ 4 : 0) ^ + (_minFresh.HasValue ? _minFresh.Value.GetHashCode() ^ 8 : 0); + + if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0)) + { + foreach (var noCacheHeader in _noCacheHeaders) + { + result = result ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(noCacheHeader); + } + } + + if ((_privateHeaders != null) && (_privateHeaders.Count > 0)) + { + foreach (var privateHeader in _privateHeaders) + { + result = result ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(privateHeader); + } + } + + if ((_extensions != null) && (_extensions.Count > 0)) + { + foreach (var extension in _extensions) + { + result = result ^ extension.GetHashCode(); + } + } + + return result; + } + + public static CacheControlHeaderValue Parse(StringSegment input) + { + int index = 0; + // Cache-Control is unusual because there are no required values so the parser will succeed for an empty string, but still return null. + var result = Parser.ParseValue(input, ref index); + if (result == null) + { + throw new FormatException("No cache directives found."); + } + return result; + } + + public static bool TryParse(StringSegment input, out CacheControlHeaderValue parsedValue) + { + int index = 0; + // Cache-Control is unusual because there are no required values so the parser will succeed for an empty string, but still return null. + if (Parser.TryParseValue(input, ref index, out parsedValue) && parsedValue != null) + { + return true; + } + parsedValue = null; + return false; + } + + private static int GetCacheControlLength(StringSegment input, int startIndex, out CacheControlHeaderValue parsedValue) + { + Contract.Requires(startIndex >= 0); + + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; + } + + // Cache-Control header consists of a list of name/value pairs, where the value is optional. So use an + // instance of NameValueHeaderParser to parse the string. + var current = startIndex; + NameValueHeaderValue nameValue = null; + var nameValueList = new List<NameValueHeaderValue>(); + while (current < input.Length) + { + if (!NameValueHeaderValue.MultipleValueParser.TryParseValue(input, ref current, out nameValue)) + { + return 0; + } + + nameValueList.Add(nameValue); + } + + // If we get here, we were able to successfully parse the string as list of name/value pairs. Now analyze + // the name/value pairs. + + // Cache-Control is a header supporting lists of values. However, expose the header as an instance of + // CacheControlHeaderValue. + var result = new CacheControlHeaderValue(); + + if (!TrySetCacheControlValues(result, nameValueList)) + { + return 0; + } + + parsedValue = result; + + // If we get here we successfully parsed the whole string. + return input.Length - startIndex; + } + + private static bool TrySetCacheControlValues( + CacheControlHeaderValue cc, + List<NameValueHeaderValue> nameValueList) + { + for (var i = 0; i < nameValueList.Count; i++) + { + var nameValue = nameValueList[i]; + var name = nameValue.Name; + var success = true; + + switch (name.Length) + { + case 6: + if (StringSegment.Equals(PublicString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._public); + } + else + { + goto default; + } + break; + + case 7: + if (StringSegment.Equals(MaxAgeString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTimeSpan(nameValue, ref cc._maxAge); + } + else if(StringSegment.Equals(PrivateString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetOptionalTokenList(nameValue, ref cc._private, ref cc._privateHeaders); + } + else + { + goto default; + } + break; + + case 8: + if (StringSegment.Equals(NoCacheString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetOptionalTokenList(nameValue, ref cc._noCache, ref cc._noCacheHeaders); + } + else if (StringSegment.Equals(NoStoreString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._noStore); + } + else if (StringSegment.Equals(SharedMaxAgeString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTimeSpan(nameValue, ref cc._sharedMaxAge); + } + else + { + goto default; + } + break; + + case 9: + if (StringSegment.Equals(MaxStaleString, name, StringComparison.OrdinalIgnoreCase)) + { + success = ((nameValue.Value == null) || TrySetTimeSpan(nameValue, ref cc._maxStaleLimit)); + if (success) + { + cc._maxStale = true; + } + } + else if (StringSegment.Equals(MinFreshString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTimeSpan(nameValue, ref cc._minFresh); + } + else + { + goto default; + } + break; + + case 12: + if (StringSegment.Equals(NoTransformString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._noTransform); + } + else + { + goto default; + } + break; + + case 14: + if (StringSegment.Equals(OnlyIfCachedString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._onlyIfCached); + } + else + { + goto default; + } + break; + + case 15: + if (StringSegment.Equals(MustRevalidateString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._mustRevalidate); + } + else + { + goto default; + } + break; + + case 16: + if (StringSegment.Equals(ProxyRevalidateString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._proxyRevalidate); + } + else + { + goto default; + } + break; + + default: + cc.Extensions.Add(nameValue); // success is always true + break; + } + + if (!success) + { + return false; + } + } + + return true; + } + + private static bool TrySetTokenOnlyValue(NameValueHeaderValue nameValue, ref bool boolField) + { + if (nameValue.Value != null) + { + return false; + } + + boolField = true; + return true; + } + + private static bool TrySetOptionalTokenList( + NameValueHeaderValue nameValue, + ref bool boolField, + ref ICollection<StringSegment> destination) + { + Contract.Requires(nameValue != null); + + if (nameValue.Value == null) + { + boolField = true; + return true; + } + + // We need the string to be at least 3 chars long: 2x quotes and at least 1 character. Also make sure we + // have a quoted string. Note that NameValueHeaderValue will never have leading/trailing whitespaces. + var valueString = nameValue.Value; + if ((valueString.Length < 3) || (valueString[0] != '\"') || (valueString[valueString.Length - 1] != '\"')) + { + return false; + } + + // We have a quoted string. Now verify that the string contains a list of valid tokens separated by ','. + var current = 1; // skip the initial '"' character. + var maxLength = valueString.Length - 1; // -1 because we don't want to parse the final '"'. + var separatorFound = false; + var originalValueCount = destination == null ? 0 : destination.Count; + while (current < maxLength) + { + current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(valueString, current, true, + out separatorFound); + + if (current == maxLength) + { + break; + } + + var tokenLength = HttpRuleParser.GetTokenLength(valueString, current); + + if (tokenLength == 0) + { + // We already skipped whitespaces and separators. If we don't have a token it must be an invalid + // character. + return false; + } + + if (destination == null) + { + destination = new ObjectCollection<StringSegment>(CheckIsValidTokenAction); + } + + destination.Add(valueString.Subsegment(current, tokenLength)); + + current = current + tokenLength; + } + + // After parsing a valid token list, we expect to have at least one value + if ((destination != null) && (destination.Count > originalValueCount)) + { + boolField = true; + return true; + } + + return false; + } + + private static bool TrySetTimeSpan(NameValueHeaderValue nameValue, ref TimeSpan? timeSpan) + { + Contract.Requires(nameValue != null); + + if (nameValue.Value == null) + { + return false; + } + + int seconds; + if (!HeaderUtilities.TryParseNonNegativeInt32(nameValue.Value, out seconds)) + { + return false; + } + + timeSpan = new TimeSpan(0, 0, seconds); + + return true; + } + + private static void AppendValueIfRequired(StringBuilder sb, bool appendValue, string value) + { + if (appendValue) + { + AppendValueWithSeparatorIfRequired(sb, value); + } + } + + private static void AppendValueWithSeparatorIfRequired(StringBuilder sb, string value) + { + if (sb.Length > 0) + { + sb.Append(", "); + } + sb.Append(value); + } + + private static void AppendValues(StringBuilder sb, IEnumerable<StringSegment> values) + { + var first = true; + foreach (var value in values) + { + if (first) + { + first = false; + } + else + { + sb.Append(", "); + } + + sb.Append(value); + } + } + + private static void CheckIsValidToken(StringSegment item) + { + HeaderUtilities.CheckValidToken(item, nameof(item)); + } + } +} diff --git a/src/Http/Headers/src/ContentDispositionHeaderValue.cs b/src/Http/Headers/src/ContentDispositionHeaderValue.cs new file mode 100644 index 0000000000000000000000000000000000000000..b9292ac1a8cdeaded5a0a6e38d44a8dd157791ae --- /dev/null +++ b/src/Http/Headers/src/ContentDispositionHeaderValue.cs @@ -0,0 +1,725 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Linq; +using System.Text; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + // Note this is for use both in HTTP (https://tools.ietf.org/html/rfc6266) and MIME (https://tools.ietf.org/html/rfc2183) + public class ContentDispositionHeaderValue + { + private const string FileNameString = "filename"; + private const string NameString = "name"; + private const string FileNameStarString = "filename*"; + private const string CreationDateString = "creation-date"; + private const string ModificationDateString = "modification-date"; + private const string ReadDateString = "read-date"; + private const string SizeString = "size"; + private static readonly char[] QuestionMark = new char[] { '?' }; + private static readonly char[] SingleQuote = new char[] { '\'' }; + + private static readonly HttpHeaderParser<ContentDispositionHeaderValue> Parser + = new GenericHeaderParser<ContentDispositionHeaderValue>(false, GetDispositionTypeLength); + + // Use list instead of dictionary since we may have multiple parameters with the same name. + private ObjectCollection<NameValueHeaderValue> _parameters; + private StringSegment _dispositionType; + + private ContentDispositionHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + + public ContentDispositionHeaderValue(StringSegment dispositionType) + { + CheckDispositionTypeFormat(dispositionType, "dispositionType"); + _dispositionType = dispositionType; + } + + public StringSegment DispositionType + { + get { return _dispositionType; } + set + { + CheckDispositionTypeFormat(value, "value"); + _dispositionType = value; + } + } + + public IList<NameValueHeaderValue> Parameters + { + get + { + if (_parameters == null) + { + _parameters = new ObjectCollection<NameValueHeaderValue>(); + } + return _parameters; + } + } + + // Helpers to access specific parameters in the list + + public StringSegment Name + { + get { return GetName(NameString); } + set { SetName(NameString, value); } + } + + + public StringSegment FileName + { + get { return GetName(FileNameString); } + set { SetName(FileNameString, value); } + } + + public StringSegment FileNameStar + { + get { return GetName(FileNameStarString); } + set { SetName(FileNameStarString, value); } + } + + public DateTimeOffset? CreationDate + { + get { return GetDate(CreationDateString); } + set { SetDate(CreationDateString, value); } + } + + public DateTimeOffset? ModificationDate + { + get { return GetDate(ModificationDateString); } + set { SetDate(ModificationDateString, value); } + } + + public DateTimeOffset? ReadDate + { + get { return GetDate(ReadDateString); } + set { SetDate(ReadDateString, value); } + } + + public long? Size + { + get + { + var sizeParameter = NameValueHeaderValue.Find(_parameters, SizeString); + long value; + if (sizeParameter != null) + { + var sizeString = sizeParameter.Value; + if (HeaderUtilities.TryParseNonNegativeInt64(sizeString, out value)) + { + return value; + } + } + return null; + } + set + { + var sizeParameter = NameValueHeaderValue.Find(_parameters, SizeString); + if (value == null) + { + // Remove parameter + if (sizeParameter != null) + { + _parameters.Remove(sizeParameter); + } + } + else if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + else if (sizeParameter != null) + { + sizeParameter.Value = value.Value.ToString(CultureInfo.InvariantCulture); + } + else + { + string sizeString = value.Value.ToString(CultureInfo.InvariantCulture); + _parameters.Add(new NameValueHeaderValue(SizeString, sizeString)); + } + } + } + + /// <summary> + /// Sets both FileName and FileNameStar using encodings appropriate for HTTP headers. + /// </summary> + /// <param name="fileName"></param> + public void SetHttpFileName(StringSegment fileName) + { + if (!StringSegment.IsNullOrEmpty(fileName)) + { + FileName = Sanatize(fileName); + } + else + { + FileName = fileName; + } + FileNameStar = fileName; + } + + /// <summary> + /// Sets the FileName parameter using encodings appropriate for MIME headers. + /// The FileNameStar paraemter is removed. + /// </summary> + /// <param name="fileName"></param> + public void SetMimeFileName(StringSegment fileName) + { + FileNameStar = null; + FileName = fileName; + } + + public override string ToString() + { + return _dispositionType + NameValueHeaderValue.ToString(_parameters, ';', true); + } + + public override bool Equals(object obj) + { + var other = obj as ContentDispositionHeaderValue; + + if (other == null) + { + return false; + } + + return _dispositionType.Equals(other._dispositionType, StringComparison.OrdinalIgnoreCase) && + HeaderUtilities.AreEqualCollections(_parameters, other._parameters); + } + + public override int GetHashCode() + { + // The dispositionType string is case-insensitive. + return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_dispositionType) ^ NameValueHeaderValue.GetHashCode(_parameters); + } + + public static ContentDispositionHeaderValue Parse(StringSegment input) + { + var index = 0; + return Parser.ParseValue(input, ref index); + } + + public static bool TryParse(StringSegment input, out ContentDispositionHeaderValue parsedValue) + { + var index = 0; + return Parser.TryParseValue(input, ref index, out parsedValue); + } + + private static int GetDispositionTypeLength(StringSegment input, int startIndex, out ContentDispositionHeaderValue parsedValue) + { + Contract.Requires(startIndex >= 0); + + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; + } + + // Caller must remove leading whitespaces. If not, we'll return 0. + var dispositionTypeLength = GetDispositionTypeExpressionLength(input, startIndex, out var dispositionType); + + if (dispositionTypeLength == 0) + { + return 0; + } + + var current = startIndex + dispositionTypeLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + var contentDispositionHeader = new ContentDispositionHeaderValue(); + contentDispositionHeader._dispositionType = dispositionType; + + // If we're not done and we have a parameter delimiter, then we have a list of parameters. + if ((current < input.Length) && (input[current] == ';')) + { + current++; // skip delimiter. + int parameterLength = NameValueHeaderValue.GetNameValueListLength(input, current, ';', + contentDispositionHeader.Parameters); + + parsedValue = contentDispositionHeader; + return current + parameterLength - startIndex; + } + + // We have a ContentDisposition header without parameters. + parsedValue = contentDispositionHeader; + return current - startIndex; + } + + private static int GetDispositionTypeExpressionLength(StringSegment input, int startIndex, out StringSegment dispositionType) + { + Contract.Requires((input != null) && (input.Length > 0) && (startIndex < input.Length)); + + // This method just parses the disposition type string, it does not parse parameters. + dispositionType = null; + + // Parse the disposition type, i.e. <dispositiontype> in content-disposition string + // "<dispositiontype>; param1=value1; param2=value2" + var typeLength = HttpRuleParser.GetTokenLength(input, startIndex); + + if (typeLength == 0) + { + return 0; + } + + dispositionType = input.Subsegment(startIndex, typeLength); + return typeLength; + } + + private static void CheckDispositionTypeFormat(StringSegment dispositionType, string parameterName) + { + if (StringSegment.IsNullOrEmpty(dispositionType)) + { + throw new ArgumentException("An empty string is not allowed.", parameterName); + } + + // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. + var dispositionTypeLength = GetDispositionTypeExpressionLength(dispositionType, 0, out var tempDispositionType); + if ((dispositionTypeLength == 0) || (tempDispositionType.Length != dispositionType.Length)) + { + throw new FormatException(string.Format(CultureInfo.InvariantCulture, + "Invalid disposition type '{0}'.", dispositionType)); + } + } + + // Gets a parameter of the given name and attempts to extract a date. + // Returns null if the parameter is not present or the format is incorrect. + private DateTimeOffset? GetDate(string parameter) + { + var dateParameter = NameValueHeaderValue.Find(_parameters, parameter); + if (dateParameter != null) + { + var dateString = dateParameter.Value; + // Should have quotes, remove them. + if (IsQuoted(dateString)) + { + dateString = dateString.Subsegment(1, dateString.Length - 2); + } + DateTimeOffset date; + if (HttpRuleParser.TryStringToDate(dateString, out date)) + { + return date; + } + } + return null; + } + + // Add the given parameter to the list. Remove if date is null. + private void SetDate(string parameter, DateTimeOffset? date) + { + var dateParameter = NameValueHeaderValue.Find(_parameters, parameter); + if (date == null) + { + // Remove parameter + if (dateParameter != null) + { + _parameters.Remove(dateParameter); + } + } + else + { + // Must always be quoted + var dateString = HeaderUtilities.FormatDate(date.Value, quoted: true); + if (dateParameter != null) + { + dateParameter.Value = dateString; + } + else + { + Parameters.Add(new NameValueHeaderValue(parameter, dateString)); + } + } + } + + // Gets a parameter of the given name and attempts to decode it if necessary. + // Returns null if the parameter is not present or the raw value if the encoding is incorrect. + private StringSegment GetName(string parameter) + { + var nameParameter = NameValueHeaderValue.Find(_parameters, parameter); + if (nameParameter != null) + { + string result; + // filename*=utf-8'lang'%7FMyString + if (parameter.EndsWith("*", StringComparison.Ordinal)) + { + if (TryDecode5987(nameParameter.Value, out result)) + { + return result; + } + return null; // Unrecognized encoding + } + + // filename="=?utf-8?B?BDFSDFasdfasdc==?=" + if (TryDecodeMime(nameParameter.Value, out result)) + { + return result; + } + // May not have been encoded + return HeaderUtilities.RemoveQuotes(nameParameter.Value); + } + return null; + } + + // Add/update the given parameter in the list, encoding if necessary. + // Remove if value is null/Empty + private void SetName(StringSegment parameter, StringSegment value) + { + var nameParameter = NameValueHeaderValue.Find(_parameters, parameter); + if (StringSegment.IsNullOrEmpty(value)) + { + // Remove parameter + if (nameParameter != null) + { + _parameters.Remove(nameParameter); + } + } + else + { + var processedValue = StringSegment.Empty; + if (parameter.EndsWith("*", StringComparison.Ordinal)) + { + processedValue = Encode5987(value); + } + else + { + processedValue = EncodeAndQuoteMime(value); + } + + if (nameParameter != null) + { + nameParameter.Value = processedValue; + } + else + { + Parameters.Add(new NameValueHeaderValue(parameter, processedValue)); + } + } + } + + // Returns input for decoding failures, as the content might not be encoded + private StringSegment EncodeAndQuoteMime(StringSegment input) + { + var result = input; + var needsQuotes = false; + // Remove bounding quotes, they'll get re-added later + if (IsQuoted(result)) + { + result = result.Subsegment(1, result.Length - 2); + needsQuotes = true; + } + + if (RequiresEncoding(result)) + { + needsQuotes = true; // Encoded data must always be quoted, the equals signs are invalid in tokens + result = EncodeMime(result); // =?utf-8?B?asdfasdfaesdf?= + } + else if (!needsQuotes && HttpRuleParser.GetTokenLength(result, 0) != result.Length) + { + needsQuotes = true; + } + + if (needsQuotes) + { + // '\' and '"' must be escaped in a quoted string + result = result.ToString().Replace(@"\", @"\\").Replace(@"""", @"\"""); + // Re-add quotes "value" + result = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", result); + } + return result; + } + + // Replaces characters not suitable for HTTP headers with '_' rather than MIME encoding them. + private StringSegment Sanatize(StringSegment input) + { + var result = input; + + if (RequiresEncoding(result)) + { + var builder = new StringBuilder(result.Length); + for (int i = 0; i < result.Length; i++) + { + var c = result[i]; + if ((int)c > 0x7f) + { + c = '_'; // Replace out-of-range characters + } + builder.Append(c); + } + result = builder.ToString(); + } + + return result; + } + + // Returns true if the value starts and ends with a quote + private bool IsQuoted(StringSegment value) + { + Contract.Assert(value != null); + + return value.Length > 1 && value.StartsWith("\"", StringComparison.Ordinal) + && value.EndsWith("\"", StringComparison.Ordinal); + } + + // tspecials are required to be in a quoted string. Only non-ascii needs to be encoded. + private bool RequiresEncoding(StringSegment input) + { + Contract.Assert(input != null); + + for (int i = 0; i < input.Length; i++) + { + if ((int)input[i] > 0x7f) + { + return true; + } + } + return false; + } + + // Encode using MIME encoding + private unsafe string EncodeMime(StringSegment input) + { + fixed (char* chars = input.Buffer) + { + var byteCount = Encoding.UTF8.GetByteCount(chars + input.Offset, input.Length); + var buffer = new byte[byteCount]; + fixed (byte* bytes = buffer) + { + Encoding.UTF8.GetBytes(chars + input.Offset, input.Length, bytes, byteCount); + } + var encodedName = Convert.ToBase64String(buffer); + return "=?utf-8?B?" + encodedName + "?="; + } + } + + // Attempt to decode MIME encoded strings + private bool TryDecodeMime(StringSegment input, out string output) + { + Contract.Assert(input != null); + + output = null; + var processedInput = input; + // Require quotes, min of "=?e?b??=" + if (!IsQuoted(processedInput) || processedInput.Length < 10) + { + return false; + } + + var parts = processedInput.Split(QuestionMark).ToArray(); + // "=, encodingName, encodingType, encodedData, =" + if (parts.Length != 5 || parts[0] != "\"=" || parts[4] != "=\"" + || !parts[2].Equals("b", StringComparison.OrdinalIgnoreCase)) + { + // Not encoded. + // This does not support multi-line encoding. + // Only base64 encoding is supported, not quoted printable + return false; + } + + try + { + var encoding = Encoding.GetEncoding(parts[1].ToString()); + var bytes = Convert.FromBase64String(parts[3].ToString()); + output = encoding.GetString(bytes, 0, bytes.Length); + return true; + } + catch (ArgumentException) + { + // Unknown encoding or bad characters + } + catch (FormatException) + { + // Bad base64 decoding + } + return false; + } + + // Encode a string using RFC 5987 encoding + // encoding'lang'PercentEncodedSpecials + private string Encode5987(StringSegment input) + { + var builder = new StringBuilder("UTF-8\'\'"); + for (int i = 0; i < input.Length; i++) + { + var c = input[i]; + // attr-char = ALPHA / DIGIT / "!" / "#" / "$" / "&" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + // ; token except ( "*" / "'" / "%" ) + if (c > 0x7F) // Encodes as multiple utf-8 bytes + { + var bytes = Encoding.UTF8.GetBytes(c.ToString()); + foreach (byte b in bytes) + { + HexEscape(builder, (char)b); + } + } + else if (!HttpRuleParser.IsTokenChar(c) || c == '*' || c == '\'' || c == '%') + { + // ASCII - Only one encoded byte + HexEscape(builder, c); + } + else + { + builder.Append(c); + } + } + return builder.ToString(); + } + + private static readonly char[] HexUpperChars = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + private static void HexEscape(StringBuilder builder, char c) + { + builder.Append('%'); + builder.Append(HexUpperChars[(c & 0xf0) >> 4]); + builder.Append(HexUpperChars[c & 0xf]); + } + + // Attempt to decode using RFC 5987 encoding. + // encoding'language'my%20string + private bool TryDecode5987(StringSegment input, out string output) + { + output = null; + + var parts = input.Split(SingleQuote).ToArray(); + if (parts.Length != 3) + { + return false; + } + + var decoded = new StringBuilder(); + byte[] unescapedBytes = null; + try + { + var encoding = Encoding.GetEncoding(parts[0].ToString()); + + var dataString = parts[2]; + unescapedBytes = ArrayPool<byte>.Shared.Rent(dataString.Length); + var unescapedBytesCount = 0; + for (var index = 0; index < dataString.Length; index++) + { + if (IsHexEncoding(dataString, index)) // %FF + { + // Unescape and cache bytes, multi-byte characters must be decoded all at once + unescapedBytes[unescapedBytesCount++] = HexUnescape(dataString, ref index); + index--; // HexUnescape did +=3; Offset the for loop's ++ + } + else + { + if (unescapedBytesCount > 0) + { + // Decode any previously cached bytes + decoded.Append(encoding.GetString(unescapedBytes, 0, unescapedBytesCount)); + unescapedBytesCount = 0; + } + decoded.Append(dataString[index]); // Normal safe character + } + } + + if (unescapedBytesCount > 0) + { + // Decode any previously cached bytes + decoded.Append(encoding.GetString(unescapedBytes, 0, unescapedBytesCount)); + } + } + catch (ArgumentException) + { + return false; // Unknown encoding or bad characters + } + finally + { + if (unescapedBytes != null) + { + ArrayPool<byte>.Shared.Return(unescapedBytes); + } + } + + output = decoded.ToString(); + return true; + } + + private static bool IsHexEncoding(StringSegment pattern, int index) + { + if ((pattern.Length - index) < 3) + { + return false; + } + if ((pattern[index] == '%') && IsEscapedAscii(pattern[index + 1], pattern[index + 2])) + { + return true; + } + return false; + } + + private static bool IsEscapedAscii(char digit, char next) + { + if (!(((digit >= '0') && (digit <= '9')) + || ((digit >= 'A') && (digit <= 'F')) + || ((digit >= 'a') && (digit <= 'f')))) + { + return false; + } + + if (!(((next >= '0') && (next <= '9')) + || ((next >= 'A') && (next <= 'F')) + || ((next >= 'a') && (next <= 'f')))) + { + return false; + } + + return true; + } + + private static byte HexUnescape(StringSegment pattern, ref int index) + { + if ((index < 0) || (index >= pattern.Length)) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + if ((pattern[index] == '%') + && (pattern.Length - index >= 3)) + { + var ret = UnEscapeAscii(pattern[index + 1], pattern[index + 2]); + index += 3; + return ret; + } + return (byte)pattern[index++]; + } + + internal static byte UnEscapeAscii(char digit, char next) + { + if (!(((digit >= '0') && (digit <= '9')) + || ((digit >= 'A') && (digit <= 'F')) + || ((digit >= 'a') && (digit <= 'f')))) + { + throw new ArgumentException(); + } + + var res = (digit <= '9') + ? ((int)digit - (int)'0') + : (((digit <= 'F') + ? ((int)digit - (int)'A') + : ((int)digit - (int)'a')) + + 10); + + if (!(((next >= '0') && (next <= '9')) + || ((next >= 'A') && (next <= 'F')) + || ((next >= 'a') && (next <= 'f')))) + { + throw new ArgumentException(); + } + + return (byte)((res << 4) + ((next <= '9') + ? ((int)next - (int)'0') + : (((next <= 'F') + ? ((int)next - (int)'A') + : ((int)next - (int)'a')) + + 10))); + } + } +} \ No newline at end of file diff --git a/src/Http/Headers/src/ContentDispositionHeaderValueIdentityExtensions.cs b/src/Http/Headers/src/ContentDispositionHeaderValueIdentityExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..9ef74baa0c11b8459cd783528684e1be24063bde --- /dev/null +++ b/src/Http/Headers/src/ContentDispositionHeaderValueIdentityExtensions.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + /// <summary> + /// Various extension methods for <see cref="ContentDispositionHeaderValue"/> for identifying the type of the disposition header + /// </summary> + public static class ContentDispositionHeaderValueIdentityExtensions + { + /// <summary> + /// Checks if the content disposition header is a file disposition + /// </summary> + /// <param name="header">The header to check</param> + /// <returns>True if the header is file disposition, false otherwise</returns> + public static bool IsFileDisposition(this ContentDispositionHeaderValue header) + { + if (header == null) + { + throw new ArgumentNullException(nameof(header)); + } + + return header.DispositionType.Equals("form-data") + && (!StringSegment.IsNullOrEmpty(header.FileName) || !StringSegment.IsNullOrEmpty(header.FileNameStar)); + } + + /// <summary> + /// Checks if the content disposition header is a form disposition + /// </summary> + /// <param name="header">The header to check</param> + /// <returns>True if the header is form disposition, false otherwise</returns> + public static bool IsFormDisposition(this ContentDispositionHeaderValue header) + { + if (header == null) + { + throw new ArgumentNullException(nameof(header)); + } + + return header.DispositionType.Equals("form-data") + && StringSegment.IsNullOrEmpty(header.FileName) && StringSegment.IsNullOrEmpty(header.FileNameStar); + } + } +} diff --git a/src/Http/Headers/src/ContentRangeHeaderValue.cs b/src/Http/Headers/src/ContentRangeHeaderValue.cs new file mode 100644 index 0000000000000000000000000000000000000000..99583cdf47039cf4cbe352d40d040f0fac4348d6 --- /dev/null +++ b/src/Http/Headers/src/ContentRangeHeaderValue.cs @@ -0,0 +1,407 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Text; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + public class ContentRangeHeaderValue + { + private static readonly HttpHeaderParser<ContentRangeHeaderValue> Parser + = new GenericHeaderParser<ContentRangeHeaderValue>(false, GetContentRangeLength); + + private StringSegment _unit; + private long? _from; + private long? _to; + private long? _length; + + private ContentRangeHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + + public ContentRangeHeaderValue(long from, long to, long length) + { + // Scenario: "Content-Range: bytes 12-34/5678" + + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + if ((to < 0) || (to > length)) + { + throw new ArgumentOutOfRangeException(nameof(to)); + } + if ((from < 0) || (from > to)) + { + throw new ArgumentOutOfRangeException(nameof(from)); + } + + _from = from; + _to = to; + _length = length; + _unit = HeaderUtilities.BytesUnit; + } + + public ContentRangeHeaderValue(long length) + { + // Scenario: "Content-Range: bytes */1234" + + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + _length = length; + _unit = HeaderUtilities.BytesUnit; + } + + public ContentRangeHeaderValue(long from, long to) + { + // Scenario: "Content-Range: bytes 12-34/*" + + if (to < 0) + { + throw new ArgumentOutOfRangeException(nameof(to)); + } + if ((from < 0) || (from > to)) + { + throw new ArgumentOutOfRangeException(nameof(@from)); + } + + _from = from; + _to = to; + _unit = HeaderUtilities.BytesUnit; + } + + public StringSegment Unit + { + get { return _unit; } + set + { + HeaderUtilities.CheckValidToken(value, nameof(value)); + _unit = value; + } + } + + public long? From + { + get { return _from; } + } + + public long? To + { + get { return _to; } + } + + public long? Length + { + get { return _length; } + } + + public bool HasLength // e.g. "Content-Range: bytes 12-34/*" + { + get { return _length != null; } + } + + public bool HasRange // e.g. "Content-Range: bytes */1234" + { + get { return _from != null; } + } + + public override bool Equals(object obj) + { + var other = obj as ContentRangeHeaderValue; + + if (other == null) + { + return false; + } + + return ((_from == other._from) && (_to == other._to) && (_length == other._length) && + StringSegment.Equals(_unit, other._unit, StringComparison.OrdinalIgnoreCase)); + } + + public override int GetHashCode() + { + var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_unit); + + if (HasRange) + { + result = result ^ _from.GetHashCode() ^ _to.GetHashCode(); + } + + if (HasLength) + { + result = result ^ _length.GetHashCode(); + } + + return result; + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(_unit); + sb.Append(' '); + + if (HasRange) + { + sb.Append(_from.Value.ToString(NumberFormatInfo.InvariantInfo)); + sb.Append('-'); + sb.Append(_to.Value.ToString(NumberFormatInfo.InvariantInfo)); + } + else + { + sb.Append('*'); + } + + sb.Append('/'); + if (HasLength) + { + sb.Append(_length.Value.ToString(NumberFormatInfo.InvariantInfo)); + } + else + { + sb.Append('*'); + } + + return sb.ToString(); + } + + public static ContentRangeHeaderValue Parse(StringSegment input) + { + var index = 0; + return Parser.ParseValue(input, ref index); + } + + public static bool TryParse(StringSegment input, out ContentRangeHeaderValue parsedValue) + { + var index = 0; + return Parser.TryParseValue(input, ref index, out parsedValue); + } + + private static int GetContentRangeLength(StringSegment input, int startIndex, out ContentRangeHeaderValue parsedValue) + { + Contract.Requires(startIndex >= 0); + + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; + } + + // Parse the unit string: <unit> in '<unit> <from>-<to>/<length>' + var unitLength = HttpRuleParser.GetTokenLength(input, startIndex); + + if (unitLength == 0) + { + return 0; + } + + var unit = input.Subsegment(startIndex, unitLength); + var current = startIndex + unitLength; + var separatorLength = HttpRuleParser.GetWhitespaceLength(input, current); + + if (separatorLength == 0) + { + return 0; + } + + current = current + separatorLength; + + if (current == input.Length) + { + return 0; + } + + // Read range values <from> and <to> in '<unit> <from>-<to>/<length>' + var fromStartIndex = current; + var fromLength = 0; + var toStartIndex = 0; + var toLength = 0; + if (!TryGetRangeLength(input, ref current, out fromLength, out toStartIndex, out toLength)) + { + return 0; + } + + // After the range is read we expect the length separator '/' + if ((current == input.Length) || (input[current] != '/')) + { + return 0; + } + + current++; // Skip '/' separator + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + if (current == input.Length) + { + return 0; + } + + // We may not have a length (e.g. 'bytes 1-2/*'). But if we do, parse the length now. + var lengthStartIndex = current; + var lengthLength = 0; + if (!TryGetLengthLength(input, ref current, out lengthLength)) + { + return 0; + } + + if (!TryCreateContentRange(input, unit, fromStartIndex, fromLength, toStartIndex, toLength, + lengthStartIndex, lengthLength, out parsedValue)) + { + return 0; + } + + return current - startIndex; + } + + private static bool TryGetLengthLength(StringSegment input, ref int current, out int lengthLength) + { + lengthLength = 0; + + if (input[current] == '*') + { + current++; + } + else + { + // Parse length value: <length> in '<unit> <from>-<to>/<length>' + lengthLength = HttpRuleParser.GetNumberLength(input, current, false); + + if ((lengthLength == 0) || (lengthLength > HttpRuleParser.MaxInt64Digits)) + { + return false; + } + + current = current + lengthLength; + } + + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + return true; + } + + private static bool TryGetRangeLength(StringSegment input, ref int current, out int fromLength, out int toStartIndex, out int toLength) + { + fromLength = 0; + toStartIndex = 0; + toLength = 0; + + // Check if we have a value like 'bytes */133'. If yes, skip the range part and continue parsing the + // length separator '/'. + if (input[current] == '*') + { + current++; + } + else + { + // Parse first range value: <from> in '<unit> <from>-<to>/<length>' + fromLength = HttpRuleParser.GetNumberLength(input, current, false); + + if ((fromLength == 0) || (fromLength > HttpRuleParser.MaxInt64Digits)) + { + return false; + } + + current = current + fromLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + // After the first value, the '-' character must follow. + if ((current == input.Length) || (input[current] != '-')) + { + // We need a '-' character otherwise this can't be a valid range. + return false; + } + + current++; // skip the '-' character + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + if (current == input.Length) + { + return false; + } + + // Parse second range value: <to> in '<unit> <from>-<to>/<length>' + toStartIndex = current; + toLength = HttpRuleParser.GetNumberLength(input, current, false); + + if ((toLength == 0) || (toLength > HttpRuleParser.MaxInt64Digits)) + { + return false; + } + + current = current + toLength; + } + + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + return true; + } + + private static bool TryCreateContentRange( + StringSegment input, + StringSegment unit, + int fromStartIndex, + int fromLength, + int toStartIndex, + int toLength, + int lengthStartIndex, + int lengthLength, + out ContentRangeHeaderValue parsedValue) + { + parsedValue = null; + + long from = 0; + if ((fromLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(fromStartIndex, fromLength), out from)) + { + return false; + } + + long to = 0; + if ((toLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(toStartIndex, toLength), out to)) + { + return false; + } + + // 'from' must not be greater than 'to' + if ((fromLength > 0) && (toLength > 0) && (from > to)) + { + return false; + } + + long length = 0; + if ((lengthLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(lengthStartIndex, lengthLength), + out length)) + { + return false; + } + + // 'from' and 'to' must be less than 'length' + if ((toLength > 0) && (lengthLength > 0) && (to >= length)) + { + return false; + } + + var result = new ContentRangeHeaderValue(); + result._unit = unit; + + if (fromLength > 0) + { + result._from = from; + result._to = to; + } + + if (lengthLength > 0) + { + result._length = length; + } + + parsedValue = result; + return true; + } + } +} diff --git a/src/Http/Headers/src/CookieHeaderParser.cs b/src/Http/Headers/src/CookieHeaderParser.cs new file mode 100644 index 0000000000000000000000000000000000000000..a94b61d319e946519d8bb87f1cb2ac5880615c3d --- /dev/null +++ b/src/Http/Headers/src/CookieHeaderParser.cs @@ -0,0 +1,98 @@ +// 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.Diagnostics.Contracts; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + internal class CookieHeaderParser : HttpHeaderParser<CookieHeaderValue> + { + internal CookieHeaderParser(bool supportsMultipleValues) + : base(supportsMultipleValues) + { + } + + public sealed override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue parsedValue) + { + parsedValue = null; + + // If multiple values are supported (i.e. list of values), then accept an empty string: The header may + // be added multiple times to the request/response message. E.g. + // Accept: text/xml; q=1 + // Accept: + // Accept: text/plain; q=0.2 + if (StringSegment.IsNullOrEmpty(value) || (index == value.Length)) + { + return SupportsMultipleValues; + } + + var separatorFound = false; + var current = GetNextNonEmptyOrWhitespaceIndex(value, index, SupportsMultipleValues, out separatorFound); + + if (separatorFound && !SupportsMultipleValues) + { + return false; // leading separators not allowed if we don't support multiple values. + } + + if (current == value.Length) + { + if (SupportsMultipleValues) + { + index = current; + } + return SupportsMultipleValues; + } + + CookieHeaderValue result = null; + if (!CookieHeaderValue.TryGetCookieLength(value, ref current, out result)) + { + return false; + } + + current = GetNextNonEmptyOrWhitespaceIndex(value, current, SupportsMultipleValues, out separatorFound); + + // If we support multiple values and we've not reached the end of the string, then we must have a separator. + if ((separatorFound && !SupportsMultipleValues) || (!separatorFound && (current < value.Length))) + { + return false; + } + + index = current; + parsedValue = result; + return true; + } + + private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int startIndex, bool skipEmptyValues, out bool separatorFound) + { + Contract.Requires(input != null); + Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length. + + separatorFound = false; + var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); + + if ((current == input.Length) || (input[current] != ',' && input[current] != ';')) + { + return current; + } + + // If we have a separator, skip the separator and all following whitespaces. If we support + // empty values, continue until the current character is neither a separator nor a whitespace. + separatorFound = true; + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + if (skipEmptyValues) + { + // Most headers only split on ',', but cookies primarily split on ';' + while ((current < input.Length) && ((input[current] == ',') || (input[current] == ';'))) + { + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + } + } + + return current; + } + } +} diff --git a/src/Http/Headers/src/CookieHeaderValue.cs b/src/Http/Headers/src/CookieHeaderValue.cs new file mode 100644 index 0000000000000000000000000000000000000000..3061b7d2fa56e5f79f7a81f3bedb39dea57da8fa --- /dev/null +++ b/src/Http/Headers/src/CookieHeaderValue.cs @@ -0,0 +1,277 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Text; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + // http://tools.ietf.org/html/rfc6265 + public class CookieHeaderValue + { + private static readonly CookieHeaderParser SingleValueParser = new CookieHeaderParser(supportsMultipleValues: false); + private static readonly CookieHeaderParser MultipleValueParser = new CookieHeaderParser(supportsMultipleValues: true); + + private StringSegment _name; + private StringSegment _value; + + private CookieHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + + public CookieHeaderValue(StringSegment name) + : this(name, StringSegment.Empty) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + } + + public CookieHeaderValue(StringSegment name, StringSegment value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + Name = name; + Value = value; + } + + public StringSegment Name + { + get { return _name; } + set + { + CheckNameFormat(value, nameof(value)); + _name = value; + } + } + + public StringSegment Value + { + get { return _value; } + set + { + CheckValueFormat(value, nameof(value)); + _value = value; + } + } + + // name="val ue"; + public override string ToString() + { + var header = new StringBuilder(); + + header.Append(_name); + header.Append("="); + header.Append(_value); + + return header.ToString(); + } + + public static CookieHeaderValue Parse(StringSegment input) + { + var index = 0; + return SingleValueParser.ParseValue(input, ref index); + } + + public static bool TryParse(StringSegment input, out CookieHeaderValue parsedValue) + { + var index = 0; + return SingleValueParser.TryParseValue(input, ref index, out parsedValue); + } + + public static IList<CookieHeaderValue> ParseList(IList<string> inputs) + { + return MultipleValueParser.ParseValues(inputs); + } + + public static IList<CookieHeaderValue> ParseStrictList(IList<string> inputs) + { + return MultipleValueParser.ParseStrictValues(inputs); + } + + public static bool TryParseList(IList<string> inputs, out IList<CookieHeaderValue> parsedValues) + { + return MultipleValueParser.TryParseValues(inputs, out parsedValues); + } + + public static bool TryParseStrictList(IList<string> inputs, out IList<CookieHeaderValue> parsedValues) + { + return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); + } + + // name=value; name="value" + internal static bool TryGetCookieLength(StringSegment input, ref int offset, out CookieHeaderValue parsedValue) + { + Contract.Requires(offset >= 0); + + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length)) + { + return false; + } + + var result = new CookieHeaderValue(); + + // The caller should have already consumed any leading whitespace, commas, etc.. + + // Name=value; + + // Name + var itemLength = HttpRuleParser.GetTokenLength(input, offset); + if (itemLength == 0) + { + return false; + } + result._name = input.Subsegment(offset, itemLength); + offset += itemLength; + + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) + { + return false; + } + + // value or "quoted value" + // The value may be empty + result._value = GetCookieValue(input, ref offset); + + parsedValue = result; + return true; + } + + // cookie-value = *cookie-octet / ( DQUOTE* cookie-octet DQUOTE ) + // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + // ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash + internal static StringSegment GetCookieValue(StringSegment input, ref int offset) + { + Contract.Requires(input != null); + Contract.Requires(offset >= 0); + Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - offset))); + + var startIndex = offset; + + if (offset >= input.Length) + { + return StringSegment.Empty; + } + var inQuotes = false; + + if (input[offset] == '"') + { + inQuotes = true; + offset++; + } + + while (offset < input.Length) + { + var c = input[offset]; + if (!IsCookieValueChar(c)) + { + break; + } + + offset++; + } + + if (inQuotes) + { + if (offset == input.Length || input[offset] != '"') + { + // Missing final quote + return StringSegment.Empty; + } + offset++; + } + + int length = offset - startIndex; + if (offset > startIndex) + { + return input.Subsegment(startIndex, length); + } + + return StringSegment.Empty; + } + + private static bool ReadEqualsSign(StringSegment input, ref int offset) + { + // = (no spaces) + if (offset >= input.Length || input[offset] != '=') + { + return false; + } + offset++; + return true; + } + + // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + // ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash + private static bool IsCookieValueChar(char c) + { + if (c < 0x21 || c > 0x7E) + { + return false; + } + return !(c == '"' || c == ',' || c == ';' || c == '\\'); + } + + internal static void CheckNameFormat(StringSegment name, string parameterName) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (HttpRuleParser.GetTokenLength(name, 0) != name.Length) + { + throw new ArgumentException("Invalid cookie name: " + name, parameterName); + } + } + + internal static void CheckValueFormat(StringSegment value, string parameterName) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + var offset = 0; + var result = GetCookieValue(value, ref offset); + if (result.Length != value.Length) + { + throw new ArgumentException("Invalid cookie value: " + value, parameterName); + } + } + + public override bool Equals(object obj) + { + var other = obj as CookieHeaderValue; + + if (other == null) + { + return false; + } + + return StringSegment.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase) + && StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase); + } + + public override int GetHashCode() + { + return _name.GetHashCode() ^ _value.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/src/Http/Headers/src/DateTimeFormatter.cs b/src/Http/Headers/src/DateTimeFormatter.cs new file mode 100644 index 0000000000000000000000000000000000000000..06893155bd3db1082209fc3de64331e9b815dd02 --- /dev/null +++ b/src/Http/Headers/src/DateTimeFormatter.cs @@ -0,0 +1,100 @@ +// 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.Globalization; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + internal static class DateTimeFormatter + { + private static readonly DateTimeFormatInfo FormatInfo = CultureInfo.InvariantCulture.DateTimeFormat; + + private static readonly string[] MonthNames = FormatInfo.AbbreviatedMonthNames; + private static readonly string[] DayNames = FormatInfo.AbbreviatedDayNames; + + private static readonly int Rfc1123DateLength = "ddd, dd MMM yyyy HH:mm:ss GMT".Length; + private static readonly int QuotedRfc1123DateLength = Rfc1123DateLength + 2; + + // ASCII numbers are in the range 48 - 57. + private const int AsciiNumberOffset = 0x30; + + private const string Gmt = "GMT"; + private const char Comma = ','; + private const char Space = ' '; + private const char Colon = ':'; + private const char Quote = '"'; + + public static string ToRfc1123String(this DateTimeOffset dateTime) + { + return ToRfc1123String(dateTime, false); + } + + public static string ToRfc1123String(this DateTimeOffset dateTime, bool quoted) + { + var universal = dateTime.UtcDateTime; + + var length = quoted ? QuotedRfc1123DateLength : Rfc1123DateLength; + var target = new InplaceStringBuilder(length); + + if (quoted) + { + target.Append(Quote); + } + + target.Append(DayNames[(int)universal.DayOfWeek]); + target.Append(Comma); + target.Append(Space); + AppendNumber(ref target, universal.Day); + target.Append(Space); + target.Append(MonthNames[universal.Month - 1]); + target.Append(Space); + AppendYear(ref target, universal.Year); + target.Append(Space); + AppendTimeOfDay(ref target, universal.TimeOfDay); + target.Append(Space); + target.Append(Gmt); + + if (quoted) + { + target.Append(Quote); + } + + return target.ToString(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendYear(ref InplaceStringBuilder target, int year) + { + target.Append(GetAsciiChar(year / 1000)); + target.Append(GetAsciiChar(year % 1000 / 100)); + target.Append(GetAsciiChar(year % 100 / 10)); + target.Append(GetAsciiChar(year % 10)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendTimeOfDay(ref InplaceStringBuilder target, TimeSpan timeOfDay) + { + AppendNumber(ref target, timeOfDay.Hours); + target.Append(Colon); + AppendNumber(ref target, timeOfDay.Minutes); + target.Append(Colon); + AppendNumber(ref target, timeOfDay.Seconds); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendNumber(ref InplaceStringBuilder target, int number) + { + target.Append(GetAsciiChar(number / 10)); + target.Append(GetAsciiChar(number % 10)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static char GetAsciiChar(int value) + { + return (char)(AsciiNumberOffset + value); + } + } +} diff --git a/src/Http/Headers/src/EntityTagHeaderValue.cs b/src/Http/Headers/src/EntityTagHeaderValue.cs new file mode 100644 index 0000000000000000000000000000000000000000..e46cee3a34ced46be4d81851fa95b08986944819 --- /dev/null +++ b/src/Http/Headers/src/EntityTagHeaderValue.cs @@ -0,0 +1,250 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + public class EntityTagHeaderValue + { + // Note that the ETag header does not allow a * but we're not that strict: We allow both '*' and ETag values in a single value. + // We can't guarantee that a single parsed value will be used directly in an ETag header. + private static readonly HttpHeaderParser<EntityTagHeaderValue> SingleValueParser + = new GenericHeaderParser<EntityTagHeaderValue>(false, GetEntityTagLength); + // Note that if multiple ETag values are allowed (e.g. 'If-Match', 'If-None-Match'), according to the RFC + // the value must either be '*' or a list of ETag values. It's not allowed to have both '*' and a list of + // ETag values. We're not that strict: We allow both '*' and ETag values in a list. If the server sends such + // an invalid list, we want to be able to represent it using the corresponding header property. + private static readonly HttpHeaderParser<EntityTagHeaderValue> MultipleValueParser + = new GenericHeaderParser<EntityTagHeaderValue>(true, GetEntityTagLength); + + private static EntityTagHeaderValue AnyType; + + private StringSegment _tag; + private bool _isWeak; + + private EntityTagHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + + public EntityTagHeaderValue(StringSegment tag) + : this(tag, false) + { + } + + public EntityTagHeaderValue(StringSegment tag, bool isWeak) + { + if (StringSegment.IsNullOrEmpty(tag)) + { + throw new ArgumentException("An empty string is not allowed.", nameof(tag)); + } + + int length = 0; + if (!isWeak && StringSegment.Equals(tag, "*", StringComparison.Ordinal)) + { + // * is valid, but W/* isn't. + _tag = tag; + } + else if ((HttpRuleParser.GetQuotedStringLength(tag, 0, out length) != HttpParseResult.Parsed) || + (length != tag.Length)) + { + // Note that we don't allow 'W/' prefixes for weak ETags in the 'tag' parameter. If the user wants to + // add a weak ETag, he can set 'isWeak' to true. + throw new FormatException("Invalid ETag name"); + } + + _tag = tag; + _isWeak = isWeak; + } + + public static EntityTagHeaderValue Any + { + get + { + if (AnyType == null) + { + AnyType = new EntityTagHeaderValue(); + AnyType._tag = "*"; + AnyType._isWeak = false; + } + return AnyType; + } + } + + public StringSegment Tag + { + get { return _tag; } + } + + public bool IsWeak + { + get { return _isWeak; } + } + + public override string ToString() + { + if (_isWeak) + { + return "W/" + _tag.ToString(); + } + return _tag.ToString(); + } + + /// <summary> + /// Check against another <see cref="EntityTagHeaderValue"/> for equality. + /// This equality check should not be used to determine if two values match under the RFC specifications (https://tools.ietf.org/html/rfc7232#section-2.3.2). + /// </summary> + /// <param name="obj">The other value to check against for equality.</param> + /// <returns> + /// <c>true</c> if the strength and tag of the two values match, + /// <c>false</c> if the other value is null, is not an <see cref="EntityTagHeaderValue"/>, or if there is a mismatch of strength or tag between the two values. + /// </returns> + public override bool Equals(object obj) + { + var other = obj as EntityTagHeaderValue; + + if (other == null) + { + return false; + } + + // Since the tag is a quoted-string we treat it case-sensitive. + return _isWeak == other._isWeak && StringSegment.Equals(_tag, other._tag, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + // Since the tag is a quoted-string we treat it case-sensitive. + return _tag.GetHashCode() ^ _isWeak.GetHashCode(); + } + + /// <summary> + /// Compares against another <see cref="EntityTagHeaderValue"/> to see if they match under the RFC specifications (https://tools.ietf.org/html/rfc7232#section-2.3.2). + /// </summary> + /// <param name="other">The other <see cref="EntityTagHeaderValue"/> to compare against.</param> + /// <param name="useStrongComparison"><c>true</c> to use a strong comparison, <c>false</c> to use a weak comparison</param> + /// <returns> + /// <c>true</c> if the <see cref="EntityTagHeaderValue"/> match for the given comparison type, + /// <c>false</c> if the other value is null or the comparison failed. + /// </returns> + public bool Compare(EntityTagHeaderValue other, bool useStrongComparison) + { + if (other == null) + { + return false; + } + + if (useStrongComparison) + { + return !IsWeak && !other.IsWeak && StringSegment.Equals(Tag, other.Tag, StringComparison.Ordinal); + } + else + { + return StringSegment.Equals(Tag, other.Tag, StringComparison.Ordinal); + } + } + + public static EntityTagHeaderValue Parse(StringSegment input) + { + var index = 0; + return SingleValueParser.ParseValue(input, ref index); + } + + public static bool TryParse(StringSegment input, out EntityTagHeaderValue parsedValue) + { + var index = 0; + return SingleValueParser.TryParseValue(input, ref index, out parsedValue); + } + + public static IList<EntityTagHeaderValue> ParseList(IList<string> inputs) + { + return MultipleValueParser.ParseValues(inputs); + } + + public static IList<EntityTagHeaderValue> ParseStrictList(IList<string> inputs) + { + return MultipleValueParser.ParseStrictValues(inputs); + } + + public static bool TryParseList(IList<string> inputs, out IList<EntityTagHeaderValue> parsedValues) + { + return MultipleValueParser.TryParseValues(inputs, out parsedValues); + } + + public static bool TryParseStrictList(IList<string> inputs, out IList<EntityTagHeaderValue> parsedValues) + { + return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); + } + + internal static int GetEntityTagLength(StringSegment input, int startIndex, out EntityTagHeaderValue parsedValue) + { + Contract.Requires(startIndex >= 0); + + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; + } + + // Caller must remove leading whitespaces. If not, we'll return 0. + var isWeak = false; + var current = startIndex; + + var firstChar = input[startIndex]; + if (firstChar == '*') + { + // We have '*' value, indicating "any" ETag. + parsedValue = Any; + current++; + } + else + { + // The RFC defines 'W/' as prefix, but we'll be flexible and also accept lower-case 'w'. + if ((firstChar == 'W') || (firstChar == 'w')) + { + current++; + // We need at least 3 more chars: the '/' character followed by two quotes. + if ((current + 2 >= input.Length) || (input[current] != '/')) + { + return 0; + } + isWeak = true; + current++; // we have a weak-entity tag. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + } + + var tagStartIndex = current; + var tagLength = 0; + if (HttpRuleParser.GetQuotedStringLength(input, current, out tagLength) != HttpParseResult.Parsed) + { + return 0; + } + + parsedValue = new EntityTagHeaderValue(); + if (tagLength == input.Length) + { + // Most of the time we'll have strong ETags without leading/trailing whitespaces. + Contract.Assert(startIndex == 0); + Contract.Assert(!isWeak); + parsedValue._tag = input; + parsedValue._isWeak = false; + } + else + { + parsedValue._tag = input.Subsegment(tagStartIndex, tagLength); + parsedValue._isWeak = isWeak; + } + + current = current + tagLength; + } + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + return current - startIndex; + } + } +} diff --git a/src/Http/Headers/src/GenericHeaderParser.cs b/src/Http/Headers/src/GenericHeaderParser.cs new file mode 100644 index 0000000000000000000000000000000000000000..a2fbf720f9f78d403f855769530e914487b00269 --- /dev/null +++ b/src/Http/Headers/src/GenericHeaderParser.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + internal sealed class GenericHeaderParser<T> : BaseHeaderParser<T> + { + internal delegate int GetParsedValueLengthDelegate(StringSegment value, int startIndex, out T parsedValue); + + private GetParsedValueLengthDelegate _getParsedValueLength; + + internal GenericHeaderParser(bool supportsMultipleValues, GetParsedValueLengthDelegate getParsedValueLength) + : base(supportsMultipleValues) + { + if (getParsedValueLength == null) + { + throw new ArgumentNullException(nameof(getParsedValueLength)); + } + + _getParsedValueLength = getParsedValueLength; + } + + protected override int GetParsedValueLength(StringSegment value, int startIndex, out T parsedValue) + { + return _getParsedValueLength(value, startIndex, out parsedValue); + } + } +} diff --git a/src/Http/Headers/src/HeaderNames.cs b/src/Http/Headers/src/HeaderNames.cs new file mode 100644 index 0000000000000000000000000000000000000000..fe79d242e8a345a4a946dc38ab6ec03334e687f9 --- /dev/null +++ b/src/Http/Headers/src/HeaderNames.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Net.Http.Headers +{ + public static class HeaderNames + { + public const string Accept = "Accept"; + public const string AcceptCharset = "Accept-Charset"; + public const string AcceptEncoding = "Accept-Encoding"; + public const string AcceptLanguage = "Accept-Language"; + public const string AcceptRanges = "Accept-Ranges"; + public const string AccessControlAllowCredentials = "Access-Control-Allow-Credentials"; + public const string AccessControlAllowHeaders = "Access-Control-Allow-Headers"; + public const string AccessControlAllowMethods = "Access-Control-Allow-Methods"; + public const string AccessControlAllowOrigin = "Access-Control-Allow-Origin"; + public const string AccessControlExposeHeaders = "Access-Control-Expose-Headers"; + public const string AccessControlMaxAge = "Access-Control-Max-Age"; + public const string AccessControlRequestHeaders = "Access-Control-Request-Headers"; + public const string AccessControlRequestMethod = "Access-Control-Request-Method"; + public const string Age = "Age"; + public const string Allow = "Allow"; + public const string Authority = ":authority"; + public const string Authorization = "Authorization"; + public const string CacheControl = "Cache-Control"; + public const string Connection = "Connection"; + public const string ContentDisposition = "Content-Disposition"; + public const string ContentEncoding = "Content-Encoding"; + public const string ContentLanguage = "Content-Language"; + public const string ContentLength = "Content-Length"; + public const string ContentLocation = "Content-Location"; + public const string ContentMD5 = "Content-MD5"; + public const string ContentRange = "Content-Range"; + public const string ContentSecurityPolicy = "Content-Security-Policy"; + public const string ContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only"; + public const string ContentType = "Content-Type"; + public const string Cookie = "Cookie"; + public const string Date = "Date"; + public const string ETag = "ETag"; + public const string Expires = "Expires"; + public const string Expect = "Expect"; + public const string From = "From"; + public const string Host = "Host"; + public const string IfMatch = "If-Match"; + public const string IfModifiedSince = "If-Modified-Since"; + public const string IfNoneMatch = "If-None-Match"; + public const string IfRange = "If-Range"; + public const string IfUnmodifiedSince = "If-Unmodified-Since"; + public const string LastModified = "Last-Modified"; + public const string Location = "Location"; + public const string MaxForwards = "Max-Forwards"; + public const string Method = ":method"; + public const string Origin = "Origin"; + public const string Path = ":path"; + public const string Pragma = "Pragma"; + public const string ProxyAuthenticate = "Proxy-Authenticate"; + public const string ProxyAuthorization = "Proxy-Authorization"; + public const string Range = "Range"; + public const string Referer = "Referer"; + public const string RetryAfter = "Retry-After"; + public const string Scheme = ":scheme"; + public const string Server = "Server"; + public const string SetCookie = "Set-Cookie"; + public const string Status = ":status"; + public const string StrictTransportSecurity = "Strict-Transport-Security"; + public const string TE = "TE"; + public const string Trailer = "Trailer"; + public const string TransferEncoding = "Transfer-Encoding"; + public const string Upgrade = "Upgrade"; + public const string UserAgent = "User-Agent"; + public const string Vary = "Vary"; + public const string Via = "Via"; + public const string Warning = "Warning"; + public const string WebSocketSubProtocols = "Sec-WebSocket-Protocol"; + public const string WWWAuthenticate = "WWW-Authenticate"; + } +} diff --git a/src/Http/Headers/src/HeaderQuality.cs b/src/Http/Headers/src/HeaderQuality.cs new file mode 100644 index 0000000000000000000000000000000000000000..da864507265b9aff52daf9cacc60e45924ce8e81 --- /dev/null +++ b/src/Http/Headers/src/HeaderQuality.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Net.Http.Headers +{ + public static class HeaderQuality + { + /// <summary> + /// Quality factor to indicate a perfect match. + /// </summary> + public const double Match = 1.0; + + /// <summary> + /// Quality factor to indicate no match. + /// </summary> + public const double NoMatch = 0.0; + } +} \ No newline at end of file diff --git a/src/Http/Headers/src/HeaderUtilities.cs b/src/Http/Headers/src/HeaderUtilities.cs new file mode 100644 index 0000000000000000000000000000000000000000..20b4319252113e3974019fdaab6b5567020dea32 --- /dev/null +++ b/src/Http/Headers/src/HeaderUtilities.cs @@ -0,0 +1,732 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Globalization; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + public static class HeaderUtilities + { + private static readonly int _int64MaxStringLength = 19; + private static readonly int _qualityValueMaxCharCount = 10; // Little bit more permissive than RFC7231 5.3.1 + private const string QualityName = "q"; + internal const string BytesUnit = "bytes"; + + internal static void SetQuality(IList<NameValueHeaderValue> parameters, double? value) + { + Contract.Requires(parameters != null); + + var qualityParameter = NameValueHeaderValue.Find(parameters, QualityName); + if (value.HasValue) + { + // Note that even if we check the value here, we can't prevent a user from adding an invalid quality + // value using Parameters.Add(). Even if we would prevent the user from adding an invalid value + // using Parameters.Add() he could always add invalid values using HttpHeaders.AddWithoutValidation(). + // So this check is really for convenience to show users that they're trying to add an invalid + // value. + if ((value < 0) || (value > 1)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + var qualityString = ((double)value).ToString("0.0##", NumberFormatInfo.InvariantInfo); + if (qualityParameter != null) + { + qualityParameter.Value = qualityString; + } + else + { + parameters.Add(new NameValueHeaderValue(QualityName, qualityString)); + } + } + else + { + // Remove quality parameter + if (qualityParameter != null) + { + parameters.Remove(qualityParameter); + } + } + } + + internal static double? GetQuality(IList<NameValueHeaderValue> parameters) + { + Contract.Requires(parameters != null); + + var qualityParameter = NameValueHeaderValue.Find(parameters, QualityName); + if (qualityParameter != null) + { + // Note that the RFC requires decimal '.' regardless of the culture. I.e. using ',' as decimal + // separator is considered invalid (even if the current culture would allow it). + if (TryParseQualityDouble(qualityParameter.Value, 0, out var qualityValue, out var length)) + + { + return qualityValue; + } + } + return null; + } + + internal static void CheckValidToken(StringSegment value, string parameterName) + { + if (StringSegment.IsNullOrEmpty(value)) + { + throw new ArgumentException("An empty string is not allowed.", parameterName); + } + + if (HttpRuleParser.GetTokenLength(value, 0) != value.Length) + { + throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid token '{0}.", value)); + } + } + + internal static bool AreEqualCollections<T>(ICollection<T> x, ICollection<T> y) + { + return AreEqualCollections(x, y, null); + } + + internal static bool AreEqualCollections<T>(ICollection<T> x, ICollection<T> y, IEqualityComparer<T> comparer) + { + if (x == null) + { + return (y == null) || (y.Count == 0); + } + + if (y == null) + { + return (x.Count == 0); + } + + if (x.Count != y.Count) + { + return false; + } + + if (x.Count == 0) + { + return true; + } + + // We have two unordered lists. So comparison is an O(n*m) operation which is expensive. Usually + // headers have 1-2 parameters (if any), so this comparison shouldn't be too expensive. + var alreadyFound = new bool[x.Count]; + var i = 0; + foreach (var xItem in x) + { + Contract.Assert(xItem != null); + + i = 0; + var found = false; + foreach (var yItem in y) + { + if (!alreadyFound[i]) + { + if (((comparer == null) && xItem.Equals(yItem)) || + ((comparer != null) && comparer.Equals(xItem, yItem))) + { + alreadyFound[i] = true; + found = true; + break; + } + } + i++; + } + + if (!found) + { + return false; + } + } + + // Since we never re-use a "found" value in 'y', we expecte 'alreadyFound' to have all fields set to 'true'. + // Otherwise the two collections can't be equal and we should not get here. + Contract.Assert(Contract.ForAll(alreadyFound, value => { return value; }), + "Expected all values in 'alreadyFound' to be true since collections are considered equal."); + + return true; + } + + internal static int GetNextNonEmptyOrWhitespaceIndex( + StringSegment input, + int startIndex, + bool skipEmptyValues, + out bool separatorFound) + { + Contract.Requires(input != null); + Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length. + + separatorFound = false; + var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); + + if ((current == input.Length) || (input[current] != ',')) + { + return current; + } + + // If we have a separator, skip the separator and all following whitespaces. If we support + // empty values, continue until the current character is neither a separator nor a whitespace. + separatorFound = true; + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + if (skipEmptyValues) + { + while ((current < input.Length) && (input[current] == ',')) + { + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + } + } + + return current; + } + + private static int AdvanceCacheDirectiveIndex(int current, string headerValue) + { + // Skip until the next potential name + current += HttpRuleParser.GetWhitespaceLength(headerValue, current); + + // Skip the value if present + if (current < headerValue.Length && headerValue[current] == '=') + { + current++; // skip '=' + current += NameValueHeaderValue.GetValueLength(headerValue, current); + } + + // Find the next delimiter + current = headerValue.IndexOf(',', current); + + if (current == -1) + { + // If no delimiter found, skip to the end + return headerValue.Length; + } + + current++; // skip ',' + current += HttpRuleParser.GetWhitespaceLength(headerValue, current); + + return current; + } + + /// <summary> + /// Try to find a target header value among the set of given header values and parse it as a + /// <see cref="TimeSpan"/>. + /// </summary> + /// <param name="headerValues"> + /// The <see cref="StringValues"/> containing the set of header values to search. + /// </param> + /// <param name="targetValue"> + /// The target header value to look for. + /// </param> + /// <param name="value"> + /// When this method returns, contains the parsed <see cref="TimeSpan"/>, if the parsing succeeded, or + /// null if the parsing failed. The conversion fails if the <paramref name="targetValue"/> was not + /// found or could not be parsed as a <see cref="TimeSpan"/>. This parameter is passed uninitialized; + /// any value originally supplied in result will be overwritten. + /// </param> + /// <returns> + /// <code>true</code> if <paramref name="targetValue"/> is found and successfully parsed; otherwise, + /// <code>false</code>. + /// </returns> + // e.g. { "headerValue=10, targetHeaderValue=30" } + public static bool TryParseSeconds(StringValues headerValues, string targetValue, out TimeSpan? value) + { + if (StringValues.IsNullOrEmpty(headerValues) || string.IsNullOrEmpty(targetValue)) + { + value = null; + return false; + } + + for (var i = 0; i < headerValues.Count; i++) + { + // Trim leading white space + var current = HttpRuleParser.GetWhitespaceLength(headerValues[i], 0); + + while (current < headerValues[i].Length) + { + long seconds; + var initial = current; + var tokenLength = HttpRuleParser.GetTokenLength(headerValues[i], current); + if (tokenLength == targetValue.Length + && string.Compare(headerValues[i], current, targetValue, 0, tokenLength, StringComparison.OrdinalIgnoreCase) == 0 + && TryParseNonNegativeInt64FromHeaderValue(current + tokenLength, headerValues[i], out seconds)) + { + // Token matches target value and seconds were parsed + value = TimeSpan.FromSeconds(seconds); + return true; + } + + current = AdvanceCacheDirectiveIndex(current + tokenLength, headerValues[i]); + + // Ensure index was advanced + if (current <= initial) + { + Debug.Assert(false, $"Index '{nameof(current)}' not advanced, this is a bug."); + value = null; + return false; + } + } + } + value = null; + return false; + } + + /// <summary> + /// Check if a target directive exists among the set of given cache control directives. + /// </summary> + /// <param name="cacheControlDirectives"> + /// The <see cref="StringValues"/> containing the set of cache control directives. + /// </param> + /// <param name="targetDirectives"> + /// The target cache control directives to look for. + /// </param> + /// <returns> + /// <code>true</code> if <paramref name="targetDirectives"/> is contained in <paramref name="cacheControlDirectives"/>; + /// otherwise, <code>false</code>. + /// </returns> + public static bool ContainsCacheDirective(StringValues cacheControlDirectives, string targetDirectives) + { + if (StringValues.IsNullOrEmpty(cacheControlDirectives) || string.IsNullOrEmpty(targetDirectives)) + { + return false; + } + + for (var i = 0; i < cacheControlDirectives.Count; i++) + { + // Trim leading white space + var current = HttpRuleParser.GetWhitespaceLength(cacheControlDirectives[i], 0); + + while (current < cacheControlDirectives[i].Length) + { + var initial = current; + + var tokenLength = HttpRuleParser.GetTokenLength(cacheControlDirectives[i], current); + if (tokenLength == targetDirectives.Length + && string.Compare(cacheControlDirectives[i], current, targetDirectives, 0, tokenLength, StringComparison.OrdinalIgnoreCase) == 0) + { + // Token matches target value + return true; + } + + current = AdvanceCacheDirectiveIndex(current + tokenLength, cacheControlDirectives[i]); + + // Ensure index was advanced + if (current <= initial) + { + Debug.Assert(false, $"Index '{nameof(current)}' not advanced, this is a bug."); + return false; + } + } + } + + return false; + } + + private static unsafe bool TryParseNonNegativeInt64FromHeaderValue(int startIndex, string headerValue, out long result) + { + // Trim leading whitespace + startIndex += HttpRuleParser.GetWhitespaceLength(headerValue, startIndex); + + // Match and skip '=', it also can't be the last character in the headerValue + if (startIndex >= headerValue.Length - 1 || headerValue[startIndex] != '=') + { + result = 0; + return false; + } + startIndex++; + + // Trim trailing whitespace + startIndex += HttpRuleParser.GetWhitespaceLength(headerValue, startIndex); + + // Try parse the number + if (TryParseNonNegativeInt64(new StringSegment(headerValue, startIndex, HttpRuleParser.GetNumberLength(headerValue, startIndex, false)), out result)) + { + return true; + } + + result = 0; + return false; + } + + /// <summary> + /// Try to convert a string representation of a positive number to its 64-bit signed integer equivalent. + /// A return value indicates whether the conversion succeeded or failed. + /// </summary> + /// <param name="value"> + /// A string containing a number to convert. + /// </param> + /// <param name="result"> + /// When this method returns, contains the 64-bit signed integer value equivalent of the number contained + /// in the string, if the conversion succeeded, or zero if the conversion failed. The conversion fails if + /// the string is null or String.Empty, is not of the correct format, is negative, or represents a number + /// greater than Int64.MaxValue. This parameter is passed uninitialized; any value originally supplied in + /// result will be overwritten. + /// </param> + /// <returns><code>true</code> if parsing succeeded; otherwise, <code>false</code>.</returns> + public static unsafe bool TryParseNonNegativeInt32(StringSegment value, out int result) + { + if (string.IsNullOrEmpty(value.Buffer) || value.Length == 0) + { + result = 0; + return false; + } + + result = 0; + fixed (char* ptr = value.Buffer) + { + var ch = (ushort*)ptr + value.Offset; + var end = ch + value.Length; + + ushort digit = 0; + while (ch < end && (digit = (ushort)(*ch - 0x30)) <= 9) + { + // Check for overflow + if ((result = result * 10 + digit) < 0) + { + result = 0; + return false; + } + + ch++; + } + + if (ch != end) + { + result = 0; + return false; + } + return true; + } + } + + /// <summary> + /// Try to convert a <see cref="StringSegment"/> representation of a positive number to its 64-bit signed + /// integer equivalent. A return value indicates whether the conversion succeeded or failed. + /// </summary> + /// <param name="value"> + /// A <see cref="StringSegment"/> containing a number to convert. + /// </param> + /// <param name="result"> + /// When this method returns, contains the 64-bit signed integer value equivalent of the number contained + /// in the string, if the conversion succeeded, or zero if the conversion failed. The conversion fails if + /// the <see cref="StringSegment"/> is null or String.Empty, is not of the correct format, is negative, or + /// represents a number greater than Int64.MaxValue. This parameter is passed uninitialized; any value + /// originally supplied in result will be overwritten. + /// </param> + /// <returns><code>true</code> if parsing succeeded; otherwise, <code>false</code>.</returns> + public static unsafe bool TryParseNonNegativeInt64(StringSegment value, out long result) + { + if (string.IsNullOrEmpty(value.Buffer) || value.Length == 0) + { + result = 0; + return false; + } + + result = 0; + fixed (char* ptr = value.Buffer) + { + var ch = (ushort*)ptr + value.Offset; + var end = ch + value.Length; + + ushort digit = 0; + while (ch < end && (digit = (ushort)(*ch - 0x30)) <= 9) + { + // Check for overflow + if ((result = result * 10 + digit) < 0) + { + result = 0; + return false; + } + + ch++; + } + + if (ch != end) + { + result = 0; + return false; + } + return true; + } + } + + // Strict and fast RFC7231 5.3.1 Quality value parser (and without memory allocation) + // See https://tools.ietf.org/html/rfc7231#section-5.3.1 + // Check is made to verify if the value is between 0 and 1 (and it returns False if the check fails). + internal static bool TryParseQualityDouble(StringSegment input, int startIndex, out double quality, out int length) + { + quality = 0; + length = 0; + + var inputLength = input.Length; + var current = startIndex; + var limit = startIndex + _qualityValueMaxCharCount; + + var intPart = 0; + var decPart = 0; + var decPow = 1; + + if (current >= inputLength) + { + return false; + } + + var ch = input[current]; + + if (ch >= '0' && ch <= '1') // Only values between 0 and 1 are accepted, according to RFC + { + intPart = ch - '0'; + current++; + } + else + { + // The RFC doesn't allow decimal values starting with dot. I.e. value ".123" is invalid. It must be in the + // form "0.123". + return false; + } + + if (current < inputLength) + { + ch = input[current]; + + if (ch >= '0' && ch <= '9') + { + // The RFC accepts only one digit before the dot + return false; + } + + if (ch == '.') + { + current++; + + while (current < inputLength) + { + ch = input[current]; + if (ch >= '0' && ch <= '9') + { + if (current >= limit) + { + return false; + } + + decPart = decPart * 10 + ch - '0'; + decPow *= 10; + current++; + } + else + { + break; + } + } + } + } + + if (decPart != 0) + { + quality = intPart + decPart / (double)decPow; + } + else + { + quality = intPart; + } + + if (quality > 1) + { + // reset quality + quality = 0; + return false; + } + + length = current - startIndex; + return true; + } + + /// <summary> + /// Converts the non-negative 64-bit numeric value to its equivalent string representation. + /// </summary> + /// <param name="value"> + /// The number to convert. + /// </param> + /// <returns> + /// The string representation of the value of this instance, consisting of a sequence of digits ranging from 0 to 9 with no leading zeroes. + /// </returns> + public unsafe static string FormatNonNegativeInt64(long value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "The value to be formatted must be non-negative."); + } + + var position = _int64MaxStringLength; + char* charBuffer = stackalloc char[_int64MaxStringLength]; + + do + { + // Consider using Math.DivRem() if available + var quotient = value / 10; + charBuffer[--position] = (char)(0x30 + (value - quotient * 10)); // 0x30 = '0' + value = quotient; + } + while (value != 0); + + return new string(charBuffer, position, _int64MaxStringLength - position); + } + + public static bool TryParseDate(StringSegment input, out DateTimeOffset result) + { + return HttpRuleParser.TryStringToDate(input, out result); + } + + public static string FormatDate(DateTimeOffset dateTime) + { + return FormatDate(dateTime, false); + } + + public static string FormatDate(DateTimeOffset dateTime, bool quoted) + { + return dateTime.ToRfc1123String(quoted); + } + + public static StringSegment RemoveQuotes(StringSegment input) + { + if (IsQuoted(input)) + { + input = input.Subsegment(1, input.Length - 2); + } + return input; + } + + public static bool IsQuoted(StringSegment input) + { + return !StringSegment.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"'; + } + + /// <summary> + /// Given a quoted-string as defined by <see href="https://tools.ietf.org/html/rfc7230#section-3.2.6">the RFC specification</see>, + /// removes quotes and unescapes backslashes and quotes. This assumes that the input is a valid quoted-string. + /// </summary> + /// <param name="input">The quoted-string to be unescaped.</param> + /// <returns>An unescaped version of the quoted-string.</returns> + public static StringSegment UnescapeAsQuotedString(StringSegment input) + { + input = RemoveQuotes(input); + + // First pass to calculate the size of the InplaceStringBuilder + var backSlashCount = CountBackslashesForDecodingQuotedString(input); + + if (backSlashCount == 0) + { + return input; + } + + var stringBuilder = new InplaceStringBuilder(input.Length - backSlashCount); + + for (var i = 0; i < input.Length; i++) + { + if (i < input.Length - 1 && input[i] == '\\') + { + // If there is an backslash character as the last character in the string, + // we will assume that it should be included literally in the unescaped string + // Ex: "hello\\" => "hello\\" + // Also, if a sender adds a quoted pair like '\\''n', + // we will assume it is over escaping and just add a n to the string. + // Ex: "he\\llo" => "hello" + stringBuilder.Append(input[i + 1]); + i++; + continue; + } + stringBuilder.Append(input[i]); + } + + return stringBuilder.ToString(); + } + + private static int CountBackslashesForDecodingQuotedString(StringSegment input) + { + var numberBackSlashes = 0; + for (var i = 0; i < input.Length; i++) + { + if (i < input.Length - 1 && input[i] == '\\') + { + // If there is an backslash character as the last character in the string, + // we will assume that it should be included literally in the unescaped string + // Ex: "hello\\" => "hello\\" + // Also, if a sender adds a quoted pair like '\\''n', + // we will assume it is over escaping and just add a n to the string. + // Ex: "he\\llo" => "hello" + if (input[i + 1] == '\\') + { + // Only count escaped backslashes once + i++; + } + numberBackSlashes++; + } + } + return numberBackSlashes; + } + + /// <summary> + /// Escapes a <see cref="StringSegment"/> as a quoted-string, which is defined by + /// <see href="https://tools.ietf.org/html/rfc7230#section-3.2.6">the RFC specification</see>. + /// </summary> + /// <remarks> + /// This will add a backslash before each backslash and quote and add quotes + /// around the input. Assumes that the input does not have quotes around it, + /// as this method will add them. Throws if the input contains any invalid escape characters, + /// as defined by rfc7230. + /// </remarks> + /// <param name="input">The input to be escaped.</param> + /// <returns>An escaped version of the quoted-string.</returns> + public static StringSegment EscapeAsQuotedString(StringSegment input) + { + // By calling this, we know that the string requires quotes around it to be a valid token. + var backSlashCount = CountAndCheckCharactersNeedingBackslashesWhenEncoding(input); + + var stringBuilder = new InplaceStringBuilder(input.Length + backSlashCount + 2); // 2 for quotes + stringBuilder.Append('\"'); + + for (var i = 0; i < input.Length; i++) + { + if (input[i] == '\\' || input[i] == '\"') + { + stringBuilder.Append('\\'); + } + else if ((input[i] <= 0x1F || input[i] == 0x7F) && input[i] != 0x09) + { + // Control characters are not allowed in a quoted-string, which include all characters + // below 0x1F (except for 0x09 (TAB)) and 0x7F. + throw new FormatException($"Invalid control character '{input[i]}' in input."); + } + stringBuilder.Append(input[i]); + } + stringBuilder.Append('\"'); + return stringBuilder.ToString(); + } + + private static int CountAndCheckCharactersNeedingBackslashesWhenEncoding(StringSegment input) + { + var numberOfCharactersNeedingEscaping = 0; + for (var i = 0; i < input.Length; i++) + { + if (input[i] == '\\' || input[i] == '\"') + { + numberOfCharactersNeedingEscaping++; + } + } + return numberOfCharactersNeedingEscaping; + } + + internal static void ThrowIfReadOnly(bool isReadOnly) + { + if (isReadOnly) + { + throw new InvalidOperationException("The object cannot be modified because it is read-only."); + } + } + } +} diff --git a/src/Http/Headers/src/HttpHeaderParser.cs b/src/Http/Headers/src/HttpHeaderParser.cs new file mode 100644 index 0000000000000000000000000000000000000000..027a9de43840378bbe375427e7396cd95de080e0 --- /dev/null +++ b/src/Http/Headers/src/HttpHeaderParser.cs @@ -0,0 +1,172 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Globalization; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + internal abstract class HttpHeaderParser<T> + { + private bool _supportsMultipleValues; + + protected HttpHeaderParser(bool supportsMultipleValues) + { + _supportsMultipleValues = supportsMultipleValues; + } + + public bool SupportsMultipleValues + { + get { return _supportsMultipleValues; } + } + + // If a parser supports multiple values, a call to ParseValue/TryParseValue should return a value for 'index' + // pointing to the next non-whitespace character after a delimiter. E.g. if called with a start index of 0 + // for string "value , second_value", then after the call completes, 'index' must point to 's', i.e. the first + // non-whitespace after the separator ','. + public abstract bool TryParseValue(StringSegment value, ref int index, out T parsedValue); + + public T ParseValue(StringSegment value, ref int index) + { + // Index may be value.Length (e.g. both 0). This may be allowed for some headers (e.g. Accept but not + // allowed by others (e.g. Content-Length). The parser has to decide if this is valid or not. + Contract.Requires((value == null) || ((index >= 0) && (index <= value.Length))); + + // If a parser returns 'null', it means there was no value, but that's valid (e.g. "Accept: "). The caller + // can ignore the value. + T result; + if (!TryParseValue(value, ref index, out result)) + { + throw new FormatException(string.Format(CultureInfo.InvariantCulture, + "The header contains invalid values at index {0}: '{1}'", index, value.Value ?? "<null>")); + } + return result; + } + + public virtual bool TryParseValues(IList<string> values, out IList<T> parsedValues) + { + return TryParseValues(values, strict: false, parsedValues: out parsedValues); + } + + public virtual bool TryParseStrictValues(IList<string> values, out IList<T> parsedValues) + { + return TryParseValues(values, strict: true, parsedValues: out parsedValues); + } + + protected virtual bool TryParseValues(IList<string> values, bool strict, out IList<T> parsedValues) + { + Contract.Assert(_supportsMultipleValues); + // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller + // can ignore the value. + parsedValues = null; + List<T> results = null; + if (values == null) + { + return false; + } + for (var i = 0; i < values.Count; i++) + { + var value = values[i]; + int index = 0; + + while (!string.IsNullOrEmpty(value) && index < value.Length) + { + T output; + if (TryParseValue(value, ref index, out output)) + { + // The entry may not contain an actual value, like " , " + if (output != null) + { + if (results == null) + { + results = new List<T>(); // Allocate it only when used + } + results.Add(output); + } + } + else if (strict) + { + return false; + } + else + { + // Skip the invalid values and keep trying. + index++; + } + } + } + if (results != null) + { + parsedValues = results; + return true; + } + return false; + } + + public virtual IList<T> ParseValues(IList<string> values) + { + return ParseValues(values, strict: false); + } + + public virtual IList<T> ParseStrictValues(IList<string> values) + { + return ParseValues(values, strict: true); + } + + protected virtual IList<T> ParseValues(IList<string> values, bool strict) + { + Contract.Assert(_supportsMultipleValues); + // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller + // can ignore the value. + var parsedValues = new List<T>(); + if (values == null) + { + return parsedValues; + } + foreach (var value in values) + { + int index = 0; + + while (!string.IsNullOrEmpty(value) && index < value.Length) + { + T output; + if (TryParseValue(value, ref index, out output)) + { + // The entry may not contain an actual value, like " , " + if (output != null) + { + parsedValues.Add(output); + } + } + else if (strict) + { + throw new FormatException(string.Format(CultureInfo.InvariantCulture, + "The header contains invalid values at index {0}: '{1}'", index, value)); + } + else + { + // Skip the invalid values and keep trying. + index++; + } + } + } + return parsedValues; + } + + // If ValueType is a custom header value type (e.g. NameValueHeaderValue) it implements ToString() correctly. + // However for existing types like int, byte[], DateTimeOffset we can't override ToString(). Therefore the + // parser provides a ToString() virtual method that can be overridden by derived types to correctly serialize + // values (e.g. byte[] to Base64 encoded string). + // The default implementation is to just call ToString() on the value itself which is the right thing to do + // for most headers (custom types, string, etc.). + public virtual string ToString(object value) + { + Contract.Requires(value != null); + + return value.ToString(); + } + } +} diff --git a/src/Http/Headers/src/HttpParseResult.cs b/src/Http/Headers/src/HttpParseResult.cs new file mode 100644 index 0000000000000000000000000000000000000000..709ae0ce8434e0ce851418b4fb86fecc3faba8d9 --- /dev/null +++ b/src/Http/Headers/src/HttpParseResult.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Net.Http.Headers +{ + internal enum HttpParseResult + { + Parsed, + NotParsed, + InvalidFormat, + } +} diff --git a/src/Http/Headers/src/HttpRuleParser.cs b/src/Http/Headers/src/HttpRuleParser.cs new file mode 100644 index 0000000000000000000000000000000000000000..3741ffa110d90af23f4245afe06e74248d5f9edf --- /dev/null +++ b/src/Http/Headers/src/HttpRuleParser.cs @@ -0,0 +1,349 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Text; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + internal static class HttpRuleParser + { + private static readonly bool[] TokenChars = CreateTokenChars(); + private const int MaxNestedCount = 5; + private static readonly string[] DateFormats = new string[] { + // "r", // RFC 1123, required output format but too strict for input + "ddd, d MMM yyyy H:m:s 'GMT'", // RFC 1123 (r, except it allows both 1 and 01 for date and time) + "ddd, d MMM yyyy H:m:s", // RFC 1123, no zone - assume GMT + "d MMM yyyy H:m:s 'GMT'", // RFC 1123, no day-of-week + "d MMM yyyy H:m:s", // RFC 1123, no day-of-week, no zone + "ddd, d MMM yy H:m:s 'GMT'", // RFC 1123, short year + "ddd, d MMM yy H:m:s", // RFC 1123, short year, no zone + "d MMM yy H:m:s 'GMT'", // RFC 1123, no day-of-week, short year + "d MMM yy H:m:s", // RFC 1123, no day-of-week, short year, no zone + + "dddd, d'-'MMM'-'yy H:m:s 'GMT'", // RFC 850, short year + "dddd, d'-'MMM'-'yy H:m:s", // RFC 850 no zone + "ddd, d'-'MMM'-'yyyy H:m:s 'GMT'", // RFC 850, long year + "ddd MMM d H:m:s yyyy", // ANSI C's asctime() format + + "ddd, d MMM yyyy H:m:s zzz", // RFC 5322 + "ddd, d MMM yyyy H:m:s", // RFC 5322 no zone + "d MMM yyyy H:m:s zzz", // RFC 5322 no day-of-week + "d MMM yyyy H:m:s", // RFC 5322 no day-of-week, no zone + }; + + internal const char CR = '\r'; + internal const char LF = '\n'; + internal const char SP = ' '; + internal const char Tab = '\t'; + internal const int MaxInt64Digits = 19; + internal const int MaxInt32Digits = 10; + + // iso-8859-1, Western European (ISO) + internal static readonly Encoding DefaultHttpEncoding = Encoding.GetEncoding("iso-8859-1"); + + private static bool[] CreateTokenChars() + { + // token = 1*<any CHAR except CTLs or separators> + // CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)> + + var tokenChars = new bool[128]; // everything is false + + for (int i = 33; i < 127; i++) // skip Space (32) & DEL (127) + { + tokenChars[i] = true; + } + + // remove separators: these are not valid token characters + tokenChars[(byte)'('] = false; + tokenChars[(byte)')'] = false; + tokenChars[(byte)'<'] = false; + tokenChars[(byte)'>'] = false; + tokenChars[(byte)'@'] = false; + tokenChars[(byte)','] = false; + tokenChars[(byte)';'] = false; + tokenChars[(byte)':'] = false; + tokenChars[(byte)'\\'] = false; + tokenChars[(byte)'"'] = false; + tokenChars[(byte)'/'] = false; + tokenChars[(byte)'['] = false; + tokenChars[(byte)']'] = false; + tokenChars[(byte)'?'] = false; + tokenChars[(byte)'='] = false; + tokenChars[(byte)'{'] = false; + tokenChars[(byte)'}'] = false; + + return tokenChars; + } + + internal static bool IsTokenChar(char character) + { + // Must be between 'space' (32) and 'DEL' (127) + if (character > 127) + { + return false; + } + + return TokenChars[character]; + } + + [Pure] + internal static int GetTokenLength(StringSegment input, int startIndex) + { + Contract.Requires(input != null); + Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - startIndex))); + + if (startIndex >= input.Length) + { + return 0; + } + + var current = startIndex; + + while (current < input.Length) + { + if (!IsTokenChar(input[current])) + { + return current - startIndex; + } + current++; + } + return input.Length - startIndex; + } + + internal static int GetWhitespaceLength(StringSegment input, int startIndex) + { + Contract.Requires(input != null); + Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - startIndex))); + + if (startIndex >= input.Length) + { + return 0; + } + + var current = startIndex; + + char c; + while (current < input.Length) + { + c = input[current]; + + if ((c == SP) || (c == Tab)) + { + current++; + continue; + } + + if (c == CR) + { + // If we have a #13 char, it must be followed by #10 and then at least one SP or HT. + if ((current + 2 < input.Length) && (input[current + 1] == LF)) + { + char spaceOrTab = input[current + 2]; + if ((spaceOrTab == SP) || (spaceOrTab == Tab)) + { + current += 3; + continue; + } + } + } + + return current - startIndex; + } + + // All characters between startIndex and the end of the string are LWS characters. + return input.Length - startIndex; + } + + internal static int GetNumberLength(StringSegment input, int startIndex, bool allowDecimal) + { + Contract.Requires(input != null); + Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); + Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - startIndex))); + + var current = startIndex; + char c; + + // If decimal values are not allowed, we pretend to have read the '.' character already. I.e. if a dot is + // found in the string, parsing will be aborted. + var haveDot = !allowDecimal; + + // The RFC doesn't allow decimal values starting with dot. I.e. value ".123" is invalid. It must be in the + // form "0.123". Also, there are no negative values defined in the RFC. So we'll just parse non-negative + // values. + // The RFC only allows decimal dots not ',' characters as decimal separators. Therefore value "1,23" is + // considered invalid and must be represented as "1.23". + if (input[current] == '.') + { + return 0; + } + + while (current < input.Length) + { + c = input[current]; + if ((c >= '0') && (c <= '9')) + { + current++; + } + else if (!haveDot && (c == '.')) + { + // Note that value "1." is valid. + haveDot = true; + current++; + } + else + { + break; + } + } + + return current - startIndex; + } + + internal static HttpParseResult GetQuotedStringLength(StringSegment input, int startIndex, out int length) + { + var nestedCount = 0; + return GetExpressionLength(input, startIndex, '"', '"', false, ref nestedCount, out length); + } + + // quoted-pair = "\" CHAR + // CHAR = <any US-ASCII character (octets 0 - 127)> + internal static HttpParseResult GetQuotedPairLength(StringSegment input, int startIndex, out int length) + { + Contract.Requires(input != null); + Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); + Contract.Ensures((Contract.ValueAtReturn(out length) >= 0) && + (Contract.ValueAtReturn(out length) <= (input.Length - startIndex))); + + length = 0; + + if (input[startIndex] != '\\') + { + return HttpParseResult.NotParsed; + } + + // Quoted-char has 2 characters. Check wheter there are 2 chars left ('\' + char) + // If so, check whether the character is in the range 0-127. If not, it's an invalid value. + if ((startIndex + 2 > input.Length) || (input[startIndex + 1] > 127)) + { + return HttpParseResult.InvalidFormat; + } + + // We don't care what the char next to '\' is. + length = 2; + return HttpParseResult.Parsed; + } + + // Try the various date formats in the order listed above. + // We should accept a wide verity of common formats, but only output RFC 1123 style dates. + internal static bool TryStringToDate(StringSegment input, out DateTimeOffset result) => + DateTimeOffset.TryParseExact(input.ToString(), DateFormats, DateTimeFormatInfo.InvariantInfo, + DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result); + + // TEXT = <any OCTET except CTLs, but including LWS> + // LWS = [CRLF] 1*( SP | HT ) + // CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)> + // + // Since we don't really care about the content of a quoted string or comment, we're more tolerant and + // allow these characters. We only want to find the delimiters ('"' for quoted string and '(', ')' for comment). + // + // 'nestedCount': Comments can be nested. We allow a depth of up to 5 nested comments, i.e. something like + // "(((((comment)))))". If we wouldn't define a limit an attacker could send a comment with hundreds of nested + // comments, resulting in a stack overflow exception. In addition having more than 1 nested comment (if any) + // is unusual. + private static HttpParseResult GetExpressionLength( + StringSegment input, + int startIndex, + char openChar, + char closeChar, + bool supportsNesting, + ref int nestedCount, + out int length) + { + Contract.Requires(input != null); + Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); + Contract.Ensures((Contract.Result<HttpParseResult>() != HttpParseResult.Parsed) || + (Contract.ValueAtReturn<int>(out length) > 0)); + + length = 0; + + if (input[startIndex] != openChar) + { + return HttpParseResult.NotParsed; + } + + var current = startIndex + 1; // Start parsing with the character next to the first open-char + while (current < input.Length) + { + // Only check whether we have a quoted char, if we have at least 3 characters left to read (i.e. + // quoted char + closing char). Otherwise the closing char may be considered part of the quoted char. + var quotedPairLength = 0; + if ((current + 2 < input.Length) && + (GetQuotedPairLength(input, current, out quotedPairLength) == HttpParseResult.Parsed)) + { + // We ignore invalid quoted-pairs. Invalid quoted-pairs may mean that it looked like a quoted pair, + // but we actually have a quoted-string: e.g. "\ü" ('\' followed by a char >127 - quoted-pair only + // allows ASCII chars after '\'; qdtext allows both '\' and >127 chars). + current = current + quotedPairLength; + continue; + } + + // If we support nested expressions and we find an open-char, then parse the nested expressions. + if (supportsNesting && (input[current] == openChar)) + { + nestedCount++; + try + { + // Check if we exceeded the number of nested calls. + if (nestedCount > MaxNestedCount) + { + return HttpParseResult.InvalidFormat; + } + + var nestedLength = 0; + HttpParseResult nestedResult = GetExpressionLength(input, current, openChar, closeChar, + supportsNesting, ref nestedCount, out nestedLength); + + switch (nestedResult) + { + case HttpParseResult.Parsed: + current += nestedLength; // add the length of the nested expression and continue. + break; + + case HttpParseResult.NotParsed: + Contract.Assert(false, "'NotParsed' is unexpected: We started nested expression " + + "parsing, because we found the open-char. So either it's a valid nested " + + "expression or it has invalid format."); + break; + + case HttpParseResult.InvalidFormat: + // If the nested expression is invalid, we can't continue, so we fail with invalid format. + return HttpParseResult.InvalidFormat; + + default: + Contract.Assert(false, "Unknown enum result: " + nestedResult); + break; + } + } + finally + { + nestedCount--; + } + } + + if (input[current] == closeChar) + { + length = current - startIndex + 1; + return HttpParseResult.Parsed; + } + current++; + } + + // We didn't see the final quote, therefore we have an invalid expression string. + return HttpParseResult.InvalidFormat; + } + } +} diff --git a/src/Http/Headers/src/MediaTypeHeaderValue.cs b/src/Http/Headers/src/MediaTypeHeaderValue.cs new file mode 100644 index 0000000000000000000000000000000000000000..32074b44cc681b7aa7d67fefb6786e0647ed68a1 --- /dev/null +++ b/src/Http/Headers/src/MediaTypeHeaderValue.cs @@ -0,0 +1,721 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Linq; +using System.Text; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + /// <summary> + /// Representation of the media type header. See <see href="https://tools.ietf.org/html/rfc6838"/>. + /// </summary> + public class MediaTypeHeaderValue + { + private const string BoundaryString = "boundary"; + private const string CharsetString = "charset"; + private const string MatchesAllString = "*/*"; + private const string QualityString = "q"; + private const string WildcardString = "*"; + + private const char ForwardSlashCharacter = '/'; + private const char PeriodCharacter = '.'; + private const char PlusCharacter = '+'; + + private static readonly char[] PeriodCharacterArray = new char[] { PeriodCharacter }; + + private static readonly HttpHeaderParser<MediaTypeHeaderValue> SingleValueParser + = new GenericHeaderParser<MediaTypeHeaderValue>(false, GetMediaTypeLength); + private static readonly HttpHeaderParser<MediaTypeHeaderValue> MultipleValueParser + = new GenericHeaderParser<MediaTypeHeaderValue>(true, GetMediaTypeLength); + + // Use a collection instead of a dictionary since we may have multiple parameters with the same name. + private ObjectCollection<NameValueHeaderValue> _parameters; + private StringSegment _mediaType; + private bool _isReadOnly; + + private MediaTypeHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + + /// <summary> + /// Initializes a <see cref="MediaTypeHeaderValue"/> instance. + /// </summary> + /// <param name="mediaType">A <see cref="StringSegment"/> representation of a media type. + /// The text provided must be a single media type without parameters. </param> + public MediaTypeHeaderValue(StringSegment mediaType) + { + CheckMediaTypeFormat(mediaType, nameof(mediaType)); + _mediaType = mediaType; + } + + /// <summary> + /// Initializes a <see cref="MediaTypeHeaderValue"/> instance. + /// </summary> + /// <param name="mediaType">A <see cref="StringSegment"/> representation of a media type. + /// The text provided must be a single media type without parameters. </param> + /// <param name="quality">The <see cref="double"/> with the quality of the media type.</param> + public MediaTypeHeaderValue(StringSegment mediaType, double quality) + : this(mediaType) + { + Quality = quality; + } + + /// <summary> + /// Gets or sets the value of the charset parameter. Returns <see cref="StringSegment.Empty"/> + /// if there is no charset. + /// </summary> + public StringSegment Charset + { + get + { + return NameValueHeaderValue.Find(_parameters, CharsetString)?.Value.Value; + } + set + { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + // We don't prevent a user from setting whitespace-only charsets. Like we can't prevent a user from + // setting a non-existing charset. + var charsetParameter = NameValueHeaderValue.Find(_parameters, CharsetString); + if (StringSegment.IsNullOrEmpty(value)) + { + // Remove charset parameter + if (charsetParameter != null) + { + Parameters.Remove(charsetParameter); + } + } + else + { + if (charsetParameter != null) + { + charsetParameter.Value = value; + } + else + { + Parameters.Add(new NameValueHeaderValue(CharsetString, value)); + } + } + } + } + + /// <summary> + /// Gets or sets the value of the Encoding parameter. Setting the Encoding will set + /// the <see cref="Charset"/> to <see cref="Encoding.WebName"/>. + /// </summary> + public Encoding Encoding + { + get + { + var charset = Charset; + if (!StringSegment.IsNullOrEmpty(charset)) + { + try + { + return Encoding.GetEncoding(charset.Value); + } + catch (ArgumentException) + { + // Invalid or not supported + } + } + return null; + } + set + { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + if (value == null) + { + Charset = null; + } + else + { + Charset = value.WebName; + } + } + } + + /// <summary> + /// Gets or sets the value of the boundary parameter. Returns <see cref="StringSegment.Empty"/> + /// if there is no boundary. + /// </summary> + public StringSegment Boundary + { + get + { + return NameValueHeaderValue.Find(_parameters, BoundaryString)?.Value ?? default(StringSegment); + } + set + { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + var boundaryParameter = NameValueHeaderValue.Find(_parameters, BoundaryString); + if (StringSegment.IsNullOrEmpty(value)) + { + // Remove charset parameter + if (boundaryParameter != null) + { + Parameters.Remove(boundaryParameter); + } + } + else + { + if (boundaryParameter != null) + { + boundaryParameter.Value = value; + } + else + { + Parameters.Add(new NameValueHeaderValue(BoundaryString, value)); + } + } + } + } + + /// <summary> + /// Gets or sets the media type's parameters. Returns an empty <see cref="IList{T}"/> + /// if there are no parameters. + /// </summary> + public IList<NameValueHeaderValue> Parameters + { + get + { + if (_parameters == null) + { + if (IsReadOnly) + { + _parameters = ObjectCollection<NameValueHeaderValue>.EmptyReadOnlyCollection; + } + else + { + _parameters = new ObjectCollection<NameValueHeaderValue>(); + } + } + return _parameters; + } + } + + /// <summary> + /// Gets or sets the value of the quality parameter. Returns null + /// if there is no quality. + /// </summary> + public double? Quality + { + get { return HeaderUtilities.GetQuality(_parameters); } + set + { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + HeaderUtilities.SetQuality(Parameters, value); + } + } + + /// <summary> + /// Gets or sets the value of the media type. Returns <see cref="StringSegment.Empty"/> + /// if there is no media type. + /// </summary> + /// <example> + /// For the media type <c>"application/json"</c>, the property gives the value + /// <c>"application/json"</c>. + /// </example> + public StringSegment MediaType + { + get { return _mediaType; } + set + { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + CheckMediaTypeFormat(value, nameof(value)); + _mediaType = value; + } + } + + /// <summary> + /// Gets the type of the <see cref="MediaTypeHeaderValue"/>. + /// </summary> + /// <example> + /// For the media type <c>"application/json"</c>, the property gives the value <c>"application"</c>. + /// </example> + /// <remarks>See <see href="https://tools.ietf.org/html/rfc6838#section-4.2"/> for more details on the type.</remarks> + public StringSegment Type + { + get + { + return _mediaType.Subsegment(0, _mediaType.IndexOf(ForwardSlashCharacter)); + } + } + + /// <summary> + /// Gets the subtype of the <see cref="MediaTypeHeaderValue"/>. + /// </summary> + /// <example> + /// For the media type <c>"application/vnd.example+json"</c>, the property gives the value + /// <c>"vnd.example+json"</c>. + /// </example> + /// <remarks>See <see href="https://tools.ietf.org/html/rfc6838#section-4.2"/> for more details on the subtype.</remarks> + public StringSegment SubType + { + get + { + return _mediaType.Subsegment(_mediaType.IndexOf(ForwardSlashCharacter) + 1); + } + } + + /// <summary> + /// Gets subtype of the <see cref="MediaTypeHeaderValue"/>, excluding any structured syntax suffix. Returns <see cref="StringSegment.Empty"/> + /// if there is no subtype without suffix. + /// </summary> + /// <example> + /// For the media type <c>"application/vnd.example+json"</c>, the property gives the value + /// <c>"vnd.example"</c>. + /// </example> + public StringSegment SubTypeWithoutSuffix + { + get + { + var subType = SubType; + var startOfSuffix = subType.LastIndexOf(PlusCharacter); + if (startOfSuffix == -1) + { + return subType; + } + else + { + return subType.Subsegment(0, startOfSuffix); + } + } + } + + /// <summary> + /// Gets the structured syntax suffix of the <see cref="MediaTypeHeaderValue"/> if it has one. + /// See <see href="https://tools.ietf.org/html/rfc6838#section-4.8">The RFC documentation on structured syntaxes.</see> + /// </summary> + /// <example> + /// For the media type <c>"application/vnd.example+json"</c>, the property gives the value + /// <c>"json"</c>. + /// </example> + public StringSegment Suffix + { + get + { + var subType = SubType; + var startOfSuffix = subType.LastIndexOf(PlusCharacter); + if (startOfSuffix == -1) + { + return default(StringSegment); + } + else + { + return subType.Subsegment(startOfSuffix + 1); + } + } + } + + + /// <summary> + /// Get a <see cref="IList{T}"/> of facets of the <see cref="MediaTypeHeaderValue"/>. Facets are a + /// period separated list of StringSegments in the <see cref="SubTypeWithoutSuffix"/>. + /// See <see href="https://tools.ietf.org/html/rfc6838#section-3">The RFC documentation on facets.</see> + /// </summary> + /// <example> + /// For the media type <c>"application/vnd.example+json"</c>, the property gives the value: + /// <c>{"vnd", "example"}</c> + /// </example> + public IEnumerable<StringSegment> Facets + { + get + { + return SubTypeWithoutSuffix.Split(PeriodCharacterArray); + } + } + + /// <summary> + /// Gets whether this <see cref="MediaTypeHeaderValue"/> matches all types. + /// </summary> + public bool MatchesAllTypes => MediaType.Equals(MatchesAllString, StringComparison.Ordinal); + + /// <summary> + /// Gets whether this <see cref="MediaTypeHeaderValue"/> matches all subtypes. + /// </summary> + /// <example> + /// For the media type <c>"application/*"</c>, this property is <c>true</c>. + /// </example> + /// <example> + /// For the media type <c>"application/json"</c>, this property is <c>false</c>. + /// </example> + public bool MatchesAllSubTypes => SubType.Equals(WildcardString, StringComparison.Ordinal); + + /// <summary> + /// Gets whether this <see cref="MediaTypeHeaderValue"/> matches all subtypes, ignoring any structured syntax suffix. + /// </summary> + /// <example> + /// For the media type <c>"application/*+json"</c>, this property is <c>true</c>. + /// </example> + /// <example> + /// For the media type <c>"application/vnd.example+json"</c>, this property is <c>false</c>. + /// </example> + public bool MatchesAllSubTypesWithoutSuffix => + SubTypeWithoutSuffix.Equals(WildcardString, StringComparison.OrdinalIgnoreCase); + + /// <summary> + /// Gets whether the <see cref="MediaTypeHeaderValue"/> is readonly. + /// </summary> + public bool IsReadOnly + { + get { return _isReadOnly; } + } + + /// <summary> + /// Gets a value indicating whether this <see cref="MediaTypeHeaderValue"/> is a subset of + /// <paramref name="otherMediaType"/>. A "subset" is defined as the same or a more specific media type + /// according to the precedence described in https://www.ietf.org/rfc/rfc2068.txt section 14.1, Accept. + /// </summary> + /// <param name="otherMediaType">The <see cref="MediaTypeHeaderValue"/> to compare.</param> + /// <returns> + /// A value indicating whether this <see cref="MediaTypeHeaderValue"/> is a subset of + /// <paramref name="otherMediaType"/>. + /// </returns> + /// <remarks> + /// For example "multipart/mixed; boundary=1234" is a subset of "multipart/mixed; boundary=1234", + /// "multipart/mixed", "multipart/*", and "*/*" but not "multipart/mixed; boundary=2345" or + /// "multipart/message; boundary=1234". + /// </remarks> + public bool IsSubsetOf(MediaTypeHeaderValue otherMediaType) + { + if (otherMediaType == null) + { + return false; + } + + // "text/plain" is a subset of "text/plain", "text/*" and "*/*". "*/*" is a subset only of "*/*". + return MatchesType(otherMediaType) && + MatchesSubtype(otherMediaType) && + MatchesParameters(otherMediaType); + } + + /// <summary> + /// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components, + /// while avoiding the cost of re-validating the components. + /// </summary> + /// <returns>A deep copy.</returns> + public MediaTypeHeaderValue Copy() + { + var other = new MediaTypeHeaderValue(); + other._mediaType = _mediaType; + + if (_parameters != null) + { + other._parameters = new ObjectCollection<NameValueHeaderValue>( + _parameters.Select(item => item.Copy())); + } + return other; + } + + /// <summary> + /// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components, + /// while avoiding the cost of re-validating the components. This copy is read-only. + /// </summary> + /// <returns>A deep, read-only, copy.</returns> + public MediaTypeHeaderValue CopyAsReadOnly() + { + if (IsReadOnly) + { + return this; + } + + var other = new MediaTypeHeaderValue(); + other._mediaType = _mediaType; + if (_parameters != null) + { + other._parameters = new ObjectCollection<NameValueHeaderValue>( + _parameters.Select(item => item.CopyAsReadOnly()), isReadOnly: true); + } + other._isReadOnly = true; + return other; + } + + public override string ToString() + { + var builder = new StringBuilder(); + builder.Append(_mediaType); + NameValueHeaderValue.ToString(_parameters, separator: ';', leadingSeparator: true, destination: builder); + return builder.ToString(); + } + + public override bool Equals(object obj) + { + var other = obj as MediaTypeHeaderValue; + + if (other == null) + { + return false; + } + + return _mediaType.Equals(other._mediaType, StringComparison.OrdinalIgnoreCase) && + HeaderUtilities.AreEqualCollections(_parameters, other._parameters); + } + + public override int GetHashCode() + { + // The media-type string is case-insensitive. + return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_mediaType) ^ NameValueHeaderValue.GetHashCode(_parameters); + } + + /// <summary> + /// Takes a media type and parses it into the <see cref="MediaTypeHeaderValue" /> and its associated parameters. + /// </summary> + /// <param name="input">The <see cref="StringSegment"/> with the media type.</param> + /// <returns>The parsed <see cref="MediaTypeHeaderValue"/>.</returns> + public static MediaTypeHeaderValue Parse(StringSegment input) + { + var index = 0; + return SingleValueParser.ParseValue(input, ref index); + } + + /// <summary> + /// Takes a media type, which can include parameters, and parses it into the <see cref="MediaTypeHeaderValue" /> and its associated parameters. + /// </summary> + /// <param name="input">The <see cref="StringSegment"/> with the media type. The media type constructed here must not have an y</param> + /// <param name="parsedValue">The parsed <see cref="MediaTypeHeaderValue"/></param> + /// <returns>True if the value was successfully parsed.</returns> + public static bool TryParse(StringSegment input, out MediaTypeHeaderValue parsedValue) + { + var index = 0; + return SingleValueParser.TryParseValue(input, ref index, out parsedValue); + } + + /// <summary> + /// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderValue"></see> and its associated parameters. + /// </summary> + /// <param name="inputs">A list of media types</param> + /// <returns>The parsed <see cref="MediaTypeHeaderValue"/>.</returns> + public static IList<MediaTypeHeaderValue> ParseList(IList<string> inputs) + { + return MultipleValueParser.ParseValues(inputs); + } + + /// <summary> + /// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderValue"></see> and its associated parameters. + /// Throws if there is invalid data in a string. + /// </summary> + /// <param name="inputs">A list of media types</param> + /// <returns>The parsed <see cref="MediaTypeHeaderValue"/>.</returns> + public static IList<MediaTypeHeaderValue> ParseStrictList(IList<string> inputs) + { + return MultipleValueParser.ParseStrictValues(inputs); + } + + /// <summary> + /// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderValue"></see> and its associated parameters. + /// </summary> + /// <param name="inputs">A list of media types</param> + /// <param name="parsedValues">The parsed <see cref="MediaTypeHeaderValue"/>.</param> + /// <returns>True if the value was successfully parsed.</returns> + public static bool TryParseList(IList<string> inputs, out IList<MediaTypeHeaderValue> parsedValues) + { + return MultipleValueParser.TryParseValues(inputs, out parsedValues); + } + + /// <summary> + /// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderValue"></see> and its associated parameters. + /// </summary> + /// <param name="inputs">A list of media types</param> + /// <param name="parsedValues">The parsed <see cref="MediaTypeHeaderValue"/>.</param> + /// <returns>True if the value was successfully parsed.</returns> + public static bool TryParseStrictList(IList<string> inputs, out IList<MediaTypeHeaderValue> parsedValues) + { + return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); + } + + private static int GetMediaTypeLength(StringSegment input, int startIndex, out MediaTypeHeaderValue parsedValue) + { + Contract.Requires(startIndex >= 0); + + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; + } + + // Caller must remove leading whitespace. If not, we'll return 0. + var mediaTypeLength = MediaTypeHeaderValue.GetMediaTypeExpressionLength(input, startIndex, out var mediaType); + + if (mediaTypeLength == 0) + { + return 0; + } + + var current = startIndex + mediaTypeLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + MediaTypeHeaderValue mediaTypeHeader = null; + + // If we're not done and we have a parameter delimiter, then we have a list of parameters. + if ((current < input.Length) && (input[current] == ';')) + { + mediaTypeHeader = new MediaTypeHeaderValue(); + mediaTypeHeader._mediaType = mediaType; + + current++; // skip delimiter. + var parameterLength = NameValueHeaderValue.GetNameValueListLength(input, current, ';', + mediaTypeHeader.Parameters); + + parsedValue = mediaTypeHeader; + return current + parameterLength - startIndex; + } + + // We have a media type without parameters. + mediaTypeHeader = new MediaTypeHeaderValue(); + mediaTypeHeader._mediaType = mediaType; + parsedValue = mediaTypeHeader; + return current - startIndex; + } + + private static int GetMediaTypeExpressionLength(StringSegment input, int startIndex, out StringSegment mediaType) + { + Contract.Requires((input != null) && (input.Length > 0) && (startIndex < input.Length)); + + // This method just parses the "type/subtype" string, it does not parse parameters. + mediaType = null; + + // Parse the type, i.e. <type> in media type string "<type>/<subtype>; param1=value1; param2=value2" + var typeLength = HttpRuleParser.GetTokenLength(input, startIndex); + + if (typeLength == 0) + { + return 0; + } + + var current = startIndex + typeLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + // Parse the separator between type and subtype + if ((current >= input.Length) || (input[current] != '/')) + { + return 0; + } + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + // Parse the subtype, i.e. <subtype> in media type string "<type>/<subtype>; param1=value1; param2=value2" + var subtypeLength = HttpRuleParser.GetTokenLength(input, current); + + if (subtypeLength == 0) + { + return 0; + } + + // If there is no whitespace between <type> and <subtype> in <type>/<subtype> get the media type using + // one Substring call. Otherwise get substrings for <type> and <subtype> and combine them. + var mediaTypeLength = current + subtypeLength - startIndex; + if (typeLength + subtypeLength + 1 == mediaTypeLength) + { + mediaType = input.Subsegment(startIndex, mediaTypeLength); + } + else + { + mediaType = input.Substring(startIndex, typeLength) + ForwardSlashCharacter + input.Substring(current, subtypeLength); + } + + return mediaTypeLength; + } + + private static void CheckMediaTypeFormat(StringSegment mediaType, string parameterName) + { + if (StringSegment.IsNullOrEmpty(mediaType)) + { + throw new ArgumentException("An empty string is not allowed.", parameterName); + } + + // When adding values using strongly typed objects, no leading/trailing LWS (whitespace) is allowed. + // Also no LWS between type and subtype is allowed. + var mediaTypeLength = GetMediaTypeExpressionLength(mediaType, 0, out var tempMediaType); + if ((mediaTypeLength == 0) || (tempMediaType.Length != mediaType.Length)) + { + throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid media type '{0}'.", mediaType)); + } + } + + private bool MatchesType(MediaTypeHeaderValue set) + { + return set.MatchesAllTypes || + set.Type.Equals(Type, StringComparison.OrdinalIgnoreCase); + } + + private bool MatchesSubtype(MediaTypeHeaderValue set) + { + if (set.MatchesAllSubTypes) + { + return true; + } + if (set.Suffix.HasValue) + { + if (Suffix.HasValue) + { + return MatchesSubtypeWithoutSuffix(set) && MatchesSubtypeSuffix(set); + } + else + { + return false; + } + } + else + { + return set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase); + } + } + + private bool MatchesSubtypeWithoutSuffix(MediaTypeHeaderValue set) + { + return set.MatchesAllSubTypesWithoutSuffix || + set.SubTypeWithoutSuffix.Equals(SubTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase); + } + + private bool MatchesParameters(MediaTypeHeaderValue set) + { + if (set._parameters != null && set._parameters.Count != 0) + { + // Make sure all parameters in the potential superset are included locally. Fine to have additional + // parameters locally; they make this one more specific. + foreach (var parameter in set._parameters) + { + if (parameter.Name.Equals(WildcardString, StringComparison.OrdinalIgnoreCase)) + { + // A parameter named "*" has no effect on media type matching, as it is only used as an indication + // that the entire media type string should be treated as a wildcard. + continue; + } + + if (parameter.Name.Equals(QualityString, StringComparison.OrdinalIgnoreCase)) + { + // "q" and later parameters are not involved in media type matching. Quoting the RFC: The first + // "q" parameter (if any) separates the media-range parameter(s) from the accept-params. + break; + } + + var localParameter = NameValueHeaderValue.Find(_parameters, parameter.Name); + if (localParameter == null) + { + // Not found. + return false; + } + + if (!StringSegment.Equals(parameter.Value, localParameter.Value, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + } + return true; + } + + private bool MatchesSubtypeSuffix(MediaTypeHeaderValue set) + { + // We don't have support for wildcards on suffixes alone (e.g., "application/entity+*") + // because there's no clear use case for it. + return set.Suffix.Equals(Suffix, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/Http/Headers/src/MediaTypeHeaderValueComparer.cs b/src/Http/Headers/src/MediaTypeHeaderValueComparer.cs new file mode 100644 index 0000000000000000000000000000000000000000..cc34640988b50e9431b446e89073d02c359641fd --- /dev/null +++ b/src/Http/Headers/src/MediaTypeHeaderValueComparer.cs @@ -0,0 +1,132 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Net.Http.Headers +{ + /// <summary> + /// Implementation of <see cref="IComparer{T}"/> that can compare accept media type header fields + /// based on their quality values (a.k.a q-values). + /// </summary> + public class MediaTypeHeaderValueComparer : IComparer<MediaTypeHeaderValue> + { + private static readonly MediaTypeHeaderValueComparer _mediaTypeComparer = + new MediaTypeHeaderValueComparer(); + + private MediaTypeHeaderValueComparer() + { + } + + public static MediaTypeHeaderValueComparer QualityComparer + { + get { return _mediaTypeComparer; } + } + + /// <inheritdoc /> + /// <remarks> + /// Performs comparisons based on the arguments' quality values + /// (aka their "q-value"). Values with identical q-values are considered equal (i.e. the result is 0) + /// with the exception that suffixed subtype wildcards are considered less than subtype wildcards, subtype wildcards + /// are considered less than specific media types and full wildcards are considered less than + /// subtype wildcards. This allows callers to sort a sequence of <see cref="MediaTypeHeaderValue"/> following + /// their q-values in the order of specific media types, subtype wildcards, and last any full wildcards. + /// </remarks> + /// <example> + /// If we had a list of media types (comma separated): { text/*;q=0.8, text/*+json;q=0.8, */*;q=1, */*;q=0.8, text/plain;q=0.8 } + /// Sorting them using Compare would return: { */*;q=0.8, text/*;q=0.8, text/*+json;q=0.8, text/plain;q=0.8, */*;q=1 } + /// </example> + public int Compare(MediaTypeHeaderValue mediaType1, MediaTypeHeaderValue mediaType2) + { + if (object.ReferenceEquals(mediaType1, mediaType2)) + { + return 0; + } + + var returnValue = CompareBasedOnQualityFactor(mediaType1, mediaType2); + + if (returnValue == 0) + { + if (!mediaType1.Type.Equals(mediaType2.Type, StringComparison.OrdinalIgnoreCase)) + { + if (mediaType1.MatchesAllTypes) + { + return -1; + } + else if (mediaType2.MatchesAllTypes) + { + return 1; + } + else if (mediaType1.MatchesAllSubTypes && !mediaType2.MatchesAllSubTypes) + { + return -1; + } + else if (!mediaType1.MatchesAllSubTypes && mediaType2.MatchesAllSubTypes) + { + return 1; + } + else if (mediaType1.MatchesAllSubTypesWithoutSuffix && !mediaType2.MatchesAllSubTypesWithoutSuffix) + { + return -1; + } + else if (!mediaType1.MatchesAllSubTypesWithoutSuffix && mediaType2.MatchesAllSubTypesWithoutSuffix) + { + return 1; + } + } + else if (!mediaType1.SubType.Equals(mediaType2.SubType, StringComparison.OrdinalIgnoreCase)) + { + if (mediaType1.MatchesAllSubTypes) + { + return -1; + } + else if (mediaType2.MatchesAllSubTypes) + { + return 1; + } + else if (mediaType1.MatchesAllSubTypesWithoutSuffix && !mediaType2.MatchesAllSubTypesWithoutSuffix) + { + return -1; + } + else if (!mediaType1.MatchesAllSubTypesWithoutSuffix && mediaType2.MatchesAllSubTypesWithoutSuffix) + { + return 1; + } + } + else if (!mediaType1.Suffix.Equals(mediaType2.Suffix, StringComparison.OrdinalIgnoreCase)) + { + if (mediaType1.MatchesAllSubTypesWithoutSuffix) + { + return -1; + } + else if (mediaType2.MatchesAllSubTypesWithoutSuffix) + { + return 1; + } + } + } + + return returnValue; + } + + private static int CompareBasedOnQualityFactor( + MediaTypeHeaderValue mediaType1, + MediaTypeHeaderValue mediaType2) + { + var mediaType1Quality = mediaType1.Quality ?? HeaderQuality.Match; + var mediaType2Quality = mediaType2.Quality ?? HeaderQuality.Match; + var qualityDifference = mediaType1Quality - mediaType2Quality; + if (qualityDifference < 0) + { + return -1; + } + else if (qualityDifference > 0) + { + return 1; + } + + return 0; + } + } +} diff --git a/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj b/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj new file mode 100644 index 0000000000000000000000000000000000000000..80b0f49989e9207f0e14d795667cb08bcd72cd63 --- /dev/null +++ b/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj @@ -0,0 +1,17 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>HTTP header parser implementations.</Description> + <TargetFramework>netstandard2.0</TargetFramework> + <NoWarn>$(NoWarn);CS1591</NoWarn> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>http</PackageTags> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.Extensions.Primitives" /> + <Reference Include="System.Buffers" /> + </ItemGroup> + +</Project> diff --git a/src/Http/Headers/src/NameValueHeaderValue.cs b/src/Http/Headers/src/NameValueHeaderValue.cs new file mode 100644 index 0000000000000000000000000000000000000000..ba197e986c3ad1a3a73f0f6661970115f2254282 --- /dev/null +++ b/src/Http/Headers/src/NameValueHeaderValue.cs @@ -0,0 +1,425 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Text; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + // According to the RFC, in places where a "parameter" is required, the value is mandatory + // (e.g. Media-Type, Accept). However, we don't introduce a dedicated type for it. So NameValueHeaderValue supports + // name-only values in addition to name/value pairs. + public class NameValueHeaderValue + { + private static readonly HttpHeaderParser<NameValueHeaderValue> SingleValueParser + = new GenericHeaderParser<NameValueHeaderValue>(false, GetNameValueLength); + internal static readonly HttpHeaderParser<NameValueHeaderValue> MultipleValueParser + = new GenericHeaderParser<NameValueHeaderValue>(true, GetNameValueLength); + + private StringSegment _name; + private StringSegment _value; + private bool _isReadOnly; + + private NameValueHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + + public NameValueHeaderValue(StringSegment name) + : this(name, null) + { + } + + public NameValueHeaderValue(StringSegment name, StringSegment value) + { + CheckNameValueFormat(name, value); + + _name = name; + _value = value; + } + + public StringSegment Name + { + get { return _name; } + } + + public StringSegment Value + { + get { return _value; } + set + { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + CheckValueFormat(value); + _value = value; + } + } + + public bool IsReadOnly { get { return _isReadOnly; } } + + /// <summary> + /// Provides a copy of this object without the cost of re-validating the values. + /// </summary> + /// <returns>A copy.</returns> + public NameValueHeaderValue Copy() + { + return new NameValueHeaderValue() + { + _name = _name, + _value = _value + }; + } + + public NameValueHeaderValue CopyAsReadOnly() + { + if (IsReadOnly) + { + return this; + } + + return new NameValueHeaderValue() + { + _name = _name, + _value = _value, + _isReadOnly = true + }; + } + + public override int GetHashCode() + { + Contract.Assert(_name != null); + + var nameHashCode = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name); + + if (!StringSegment.IsNullOrEmpty(_value)) + { + // If we have a quoted-string, then just use the hash code. If we have a token, convert to lowercase + // and retrieve the hash code. + if (_value[0] == '"') + { + return nameHashCode ^ _value.GetHashCode(); + } + + return nameHashCode ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value); + } + + return nameHashCode; + } + + public override bool Equals(object obj) + { + var other = obj as NameValueHeaderValue; + + if (other == null) + { + return false; + } + + if (!_name.Equals(other._name, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // RFC2616: 14.20: unquoted tokens should use case-INsensitive comparison; quoted-strings should use + // case-sensitive comparison. The RFC doesn't mention how to compare quoted-strings outside the "Expect" + // header. We treat all quoted-strings the same: case-sensitive comparison. + + if (StringSegment.IsNullOrEmpty(_value)) + { + return StringSegment.IsNullOrEmpty(other._value); + } + + if (_value[0] == '"') + { + // We have a quoted string, so we need to do case-sensitive comparison. + return (_value.Equals(other._value, StringComparison.Ordinal)); + } + else + { + return (_value.Equals(other._value, StringComparison.OrdinalIgnoreCase)); + } + } + + public StringSegment GetUnescapedValue() + { + if (!HeaderUtilities.IsQuoted(_value)) + { + return _value; + } + return HeaderUtilities.UnescapeAsQuotedString(_value); + } + + public void SetAndEscapeValue(StringSegment value) + { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + if (StringSegment.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length)) + { + _value = value; + } + else + { + Value = HeaderUtilities.EscapeAsQuotedString(value); + } + } + + public static NameValueHeaderValue Parse(StringSegment input) + { + var index = 0; + return SingleValueParser.ParseValue(input, ref index); + } + + public static bool TryParse(StringSegment input, out NameValueHeaderValue parsedValue) + { + var index = 0; + return SingleValueParser.TryParseValue(input, ref index, out parsedValue); + } + + public static IList<NameValueHeaderValue> ParseList(IList<string> input) + { + return MultipleValueParser.ParseValues(input); + } + + public static IList<NameValueHeaderValue> ParseStrictList(IList<string> input) + { + return MultipleValueParser.ParseStrictValues(input); + } + + public static bool TryParseList(IList<string> input, out IList<NameValueHeaderValue> parsedValues) + { + return MultipleValueParser.TryParseValues(input, out parsedValues); + } + + public static bool TryParseStrictList(IList<string> input, out IList<NameValueHeaderValue> parsedValues) + { + return MultipleValueParser.TryParseStrictValues(input, out parsedValues); + } + + public override string ToString() + { + if (!StringSegment.IsNullOrEmpty(_value)) + { + return _name + "=" + _value; + } + return _name.ToString(); + } + + internal static void ToString( + IList<NameValueHeaderValue> values, + char separator, + bool leadingSeparator, + StringBuilder destination) + { + Contract.Assert(destination != null); + + if ((values == null) || (values.Count == 0)) + { + return; + } + + for (var i = 0; i < values.Count; i++) + { + if (leadingSeparator || (destination.Length > 0)) + { + destination.Append(separator); + destination.Append(' '); + } + destination.Append(values[i].Name); + if (!StringSegment.IsNullOrEmpty(values[i].Value)) + { + destination.Append('='); + destination.Append(values[i].Value); + } + } + } + + internal static string ToString(IList<NameValueHeaderValue> values, char separator, bool leadingSeparator) + { + if ((values == null) || (values.Count == 0)) + { + return null; + } + + var sb = new StringBuilder(); + + ToString(values, separator, leadingSeparator, sb); + + return sb.ToString(); + } + + internal static int GetHashCode(IList<NameValueHeaderValue> values) + { + if ((values == null) || (values.Count == 0)) + { + return 0; + } + + var result = 0; + for (var i = 0; i < values.Count; i++) + { + result = result ^ values[i].GetHashCode(); + } + return result; + } + + private static int GetNameValueLength(StringSegment input, int startIndex, out NameValueHeaderValue parsedValue) + { + Contract.Requires(input != null); + Contract.Requires(startIndex >= 0); + + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; + } + + // Parse the name, i.e. <name> in name/value string "<name>=<value>". Caller must remove + // leading whitespaces. + var nameLength = HttpRuleParser.GetTokenLength(input, startIndex); + + if (nameLength == 0) + { + return 0; + } + + var name = input.Subsegment(startIndex, nameLength); + var current = startIndex + nameLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + // Parse the separator between name and value + if ((current == input.Length) || (input[current] != '=')) + { + // We only have a name and that's OK. Return. + parsedValue = new NameValueHeaderValue(); + parsedValue._name = name; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces + return current - startIndex; + } + + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + // Parse the value, i.e. <value> in name/value string "<name>=<value>" + int valueLength = GetValueLength(input, current); + + // Value after the '=' may be empty + // Use parameterless ctor to avoid double-parsing of name and value, i.e. skip public ctor validation. + parsedValue = new NameValueHeaderValue(); + parsedValue._name = name; + parsedValue._value = input.Subsegment(current, valueLength); + current = current + valueLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces + return current - startIndex; + } + + // Returns the length of a name/value list, separated by 'delimiter'. E.g. "a=b, c=d, e=f" adds 3 + // name/value pairs to 'nameValueCollection' if 'delimiter' equals ','. + internal static int GetNameValueListLength( + StringSegment input, + int startIndex, + char delimiter, + IList<NameValueHeaderValue> nameValueCollection) + { + Contract.Requires(nameValueCollection != null); + Contract.Requires(startIndex >= 0); + + if ((StringSegment.IsNullOrEmpty(input)) || (startIndex >= input.Length)) + { + return 0; + } + + var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); + while (true) + { + NameValueHeaderValue parameter = null; + var nameValueLength = GetNameValueLength(input, current, out parameter); + + if (nameValueLength == 0) + { + // There may be a trailing ';' + return current - startIndex; + } + + nameValueCollection.Add(parameter); + current = current + nameValueLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + if ((current == input.Length) || (input[current] != delimiter)) + { + // We're done and we have at least one valid name/value pair. + return current - startIndex; + } + + // input[current] is 'delimiter'. Skip the delimiter and whitespaces and try to parse again. + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + } + } + + public static NameValueHeaderValue Find(IList<NameValueHeaderValue> values, StringSegment name) + { + Contract.Requires((name != null) && (name.Length > 0)); + + if ((values == null) || (values.Count == 0)) + { + return null; + } + + for (var i = 0; i < values.Count; i++) + { + var value = values[i]; + if (value.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + return value; + } + } + return null; + } + + internal static int GetValueLength(StringSegment input, int startIndex) + { + Contract.Requires(input != null); + + if (startIndex >= input.Length) + { + return 0; + } + + var valueLength = HttpRuleParser.GetTokenLength(input, startIndex); + + if (valueLength == 0) + { + // A value can either be a token or a quoted string. Check if it is a quoted string. + if (HttpRuleParser.GetQuotedStringLength(input, startIndex, out valueLength) != HttpParseResult.Parsed) + { + // We have an invalid value. Reset the name and return. + return 0; + } + } + return valueLength; + } + + private static void CheckNameValueFormat(StringSegment name, StringSegment value) + { + HeaderUtilities.CheckValidToken(name, nameof(name)); + CheckValueFormat(value); + } + + private static void CheckValueFormat(StringSegment value) + { + // Either value is null/empty or a valid token/quoted string + if (!(StringSegment.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length))) + { + throw new FormatException(string.Format(CultureInfo.InvariantCulture, "The header value is invalid: '{0}'", value)); + } + } + + private static NameValueHeaderValue CreateNameValue() + { + return new NameValueHeaderValue(); + } + } +} diff --git a/src/Http/Headers/src/ObjectCollection.cs b/src/Http/Headers/src/ObjectCollection.cs new file mode 100644 index 0000000000000000000000000000000000000000..db5f876b53d3a7cd1fa04eae04865dfa3049abdb --- /dev/null +++ b/src/Http/Headers/src/ObjectCollection.cs @@ -0,0 +1,91 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Microsoft.Net.Http.Headers +{ + // List<T> allows 'null' values to be added. This is not what we want so we use a custom Collection<T> derived + // type to throw if 'null' gets added. Collection<T> internally uses List<T> which comes at some cost. In addition + // Collection<T>.Add() calls List<T>.InsertItem() which is an O(n) operation (compared to O(1) for List<T>.Add()). + // This type is only used for very small collections (1-2 items) to keep the impact of using Collection<T> small. + internal class ObjectCollection<T> : Collection<T> + { + internal static readonly Action<T> DefaultValidator = CheckNotNull; + internal static readonly ObjectCollection<T> EmptyReadOnlyCollection + = new ObjectCollection<T>(DefaultValidator, isReadOnly: true); + + private readonly Action<T> _validator; + + // We need to create a 'read-only' inner list for Collection<T> to do the right + // thing. + private static IList<T> CreateInnerList(bool isReadOnly, IEnumerable <T> other = null) + { + var list = other == null ? new List<T>() : new List<T>(other); + if (isReadOnly) + { + return new ReadOnlyCollection<T>(list); + } + else + { + return list; + } + } + + public ObjectCollection() + : this(DefaultValidator) + { + } + + public ObjectCollection(Action<T> validator, bool isReadOnly = false) + : base(CreateInnerList(isReadOnly)) + { + _validator = validator; + } + + public ObjectCollection(IEnumerable<T> other, bool isReadOnly = false) + : base(CreateInnerList(isReadOnly, other)) + { + _validator = DefaultValidator; + foreach (T item in Items) + { + _validator(item); + } + } + + public bool IsReadOnly => ((ICollection<T>)this).IsReadOnly; + + protected override void ClearItems() + { + base.ClearItems(); + } + + protected override void InsertItem(int index, T item) + { + _validator(item); + base.InsertItem(index, item); + } + + protected override void RemoveItem(int index) + { + base.RemoveItem(index); + } + + protected override void SetItem(int index, T item) + { + _validator(item); + base.SetItem(index, item); + } + + private static void CheckNotNull(T item) + { + // null values cannot be added to the collection. + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Headers/src/Properties/AssemblyInfo.cs b/src/Http/Headers/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..c876def487f0176ffb2ac6b81cad26ffb1b042b0 --- /dev/null +++ b/src/Http/Headers/src/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Net.Http.Headers.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Http/Headers/src/RangeConditionHeaderValue.cs b/src/Http/Headers/src/RangeConditionHeaderValue.cs new file mode 100644 index 0000000000000000000000000000000000000000..f1ebee276c587c94dd94830bd4fa9624fea7ea2e --- /dev/null +++ b/src/Http/Headers/src/RangeConditionHeaderValue.cs @@ -0,0 +1,167 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.Contracts; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + public class RangeConditionHeaderValue + { + private static readonly HttpHeaderParser<RangeConditionHeaderValue> Parser + = new GenericHeaderParser<RangeConditionHeaderValue>(false, GetRangeConditionLength); + + private DateTimeOffset? _lastModified; + private EntityTagHeaderValue _entityTag; + + private RangeConditionHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + + public RangeConditionHeaderValue(DateTimeOffset lastModified) + { + _lastModified = lastModified; + } + + public RangeConditionHeaderValue(EntityTagHeaderValue entityTag) + { + if (entityTag == null) + { + throw new ArgumentNullException(nameof(entityTag)); + } + + _entityTag = entityTag; + } + + public RangeConditionHeaderValue(string entityTag) + : this(new EntityTagHeaderValue(entityTag)) + { + } + + public DateTimeOffset? LastModified + { + get { return _lastModified; } + } + + public EntityTagHeaderValue EntityTag + { + get { return _entityTag; } + } + + public override string ToString() + { + if (_entityTag == null) + { + return HeaderUtilities.FormatDate(_lastModified.Value); + } + return _entityTag.ToString(); + } + + public override bool Equals(object obj) + { + var other = obj as RangeConditionHeaderValue; + + if (other == null) + { + return false; + } + + if (_entityTag == null) + { + return (other._lastModified != null) && (_lastModified.Value == other._lastModified.Value); + } + + return _entityTag.Equals(other._entityTag); + } + + public override int GetHashCode() + { + if (_entityTag == null) + { + return _lastModified.Value.GetHashCode(); + } + + return _entityTag.GetHashCode(); + } + + public static RangeConditionHeaderValue Parse(StringSegment input) + { + var index = 0; + return Parser.ParseValue(input, ref index); + } + + public static bool TryParse(StringSegment input, out RangeConditionHeaderValue parsedValue) + { + var index = 0; + return Parser.TryParseValue(input, ref index, out parsedValue); + } + + private static int GetRangeConditionLength(StringSegment input, int startIndex, out RangeConditionHeaderValue parsedValue) + { + Contract.Requires(startIndex >= 0); + + parsedValue = null; + + // Make sure we have at least 2 characters + if (StringSegment.IsNullOrEmpty(input) || (startIndex + 1 >= input.Length)) + { + return 0; + } + + var current = startIndex; + + // Caller must remove leading whitespaces. + DateTimeOffset date = DateTimeOffset.MinValue; + EntityTagHeaderValue entityTag = null; + + // Entity tags are quoted strings optionally preceded by "W/". By looking at the first two character we + // can determine whether the string is en entity tag or a date. + var firstChar = input[current]; + var secondChar = input[current + 1]; + + if ((firstChar == '\"') || (((firstChar == 'w') || (firstChar == 'W')) && (secondChar == '/'))) + { + // trailing whitespaces are removed by GetEntityTagLength() + var entityTagLength = EntityTagHeaderValue.GetEntityTagLength(input, current, out entityTag); + + if (entityTagLength == 0) + { + return 0; + } + + current = current + entityTagLength; + + // RangeConditionHeaderValue only allows 1 value. There must be no delimiter/other chars after an + // entity tag. + if (current != input.Length) + { + return 0; + } + } + else + { + if (!HttpRuleParser.TryStringToDate(input.Subsegment(current), out date)) + { + return 0; + } + + // If we got a valid date, then the parser consumed the whole string (incl. trailing whitespaces). + current = input.Length; + } + + parsedValue = new RangeConditionHeaderValue(); + if (entityTag == null) + { + parsedValue._lastModified = date; + } + else + { + parsedValue._entityTag = entityTag; + } + + return current - startIndex; + } + } +} \ No newline at end of file diff --git a/src/Http/Headers/src/RangeHeaderValue.cs b/src/Http/Headers/src/RangeHeaderValue.cs new file mode 100644 index 0000000000000000000000000000000000000000..934b6b6cc17b0954d1e1f05a2acd9377fba0ef93 --- /dev/null +++ b/src/Http/Headers/src/RangeHeaderValue.cs @@ -0,0 +1,163 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Text; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + public class RangeHeaderValue + { + private static readonly HttpHeaderParser<RangeHeaderValue> Parser + = new GenericHeaderParser<RangeHeaderValue>(false, GetRangeLength); + + private StringSegment _unit; + private ICollection<RangeItemHeaderValue> _ranges; + + public RangeHeaderValue() + { + _unit = HeaderUtilities.BytesUnit; + } + + public RangeHeaderValue(long? from, long? to) + { + // convenience ctor: "Range: bytes=from-to" + _unit = HeaderUtilities.BytesUnit; + Ranges.Add(new RangeItemHeaderValue(from, to)); + } + + public StringSegment Unit + { + get { return _unit; } + set + { + HeaderUtilities.CheckValidToken(value, nameof(value)); + _unit = value; + } + } + + public ICollection<RangeItemHeaderValue> Ranges + { + get + { + if (_ranges == null) + { + _ranges = new ObjectCollection<RangeItemHeaderValue>(); + } + return _ranges; + } + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(_unit); + sb.Append('='); + + var first = true; + foreach (var range in Ranges) + { + if (first) + { + first = false; + } + else + { + sb.Append(", "); + } + + sb.Append(range.From); + sb.Append('-'); + sb.Append(range.To); + } + + return sb.ToString(); + } + + public override bool Equals(object obj) + { + var other = obj as RangeHeaderValue; + + if (other == null) + { + return false; + } + + return StringSegment.Equals(_unit, other._unit, StringComparison.OrdinalIgnoreCase) && + HeaderUtilities.AreEqualCollections(Ranges, other.Ranges); + } + + public override int GetHashCode() + { + var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_unit); + + foreach (var range in Ranges) + { + result = result ^ range.GetHashCode(); + } + + return result; + } + + public static RangeHeaderValue Parse(StringSegment input) + { + var index = 0; + return Parser.ParseValue(input, ref index); + } + + public static bool TryParse(StringSegment input, out RangeHeaderValue parsedValue) + { + var index = 0; + return Parser.TryParseValue(input, ref index, out parsedValue); + } + + private static int GetRangeLength(StringSegment input, int startIndex, out RangeHeaderValue parsedValue) + { + Contract.Requires(startIndex >= 0); + + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; + } + + // Parse the unit string: <unit> in '<unit>=<from1>-<to1>, <from2>-<to2>' + var unitLength = HttpRuleParser.GetTokenLength(input, startIndex); + + if (unitLength == 0) + { + return 0; + } + + RangeHeaderValue result = new RangeHeaderValue(); + result._unit = input.Subsegment(startIndex, unitLength); + var current = startIndex + unitLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + if ((current == input.Length) || (input[current] != '=')) + { + return 0; + } + + current++; // skip '=' separator + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + var rangesLength = RangeItemHeaderValue.GetRangeItemListLength(input, current, result.Ranges); + + if (rangesLength == 0) + { + return 0; + } + + current = current + rangesLength; + Contract.Assert(current == input.Length, "GetRangeItemListLength() should consume the whole string or fail."); + + parsedValue = result; + return current - startIndex; + } + } +} diff --git a/src/Http/Headers/src/RangeItemHeaderValue.cs b/src/Http/Headers/src/RangeItemHeaderValue.cs new file mode 100644 index 0000000000000000000000000000000000000000..99fdbfef5c13e1307bde8d79d2a8aec4a4767616 --- /dev/null +++ b/src/Http/Headers/src/RangeItemHeaderValue.cs @@ -0,0 +1,229 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Globalization; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + public class RangeItemHeaderValue + { + private long? _from; + private long? _to; + + public RangeItemHeaderValue(long? from, long? to) + { + if (!from.HasValue && !to.HasValue) + { + throw new ArgumentException("Invalid header range."); + } + if (from.HasValue && (from.Value < 0)) + { + throw new ArgumentOutOfRangeException(nameof(from)); + } + if (to.HasValue && (to.Value < 0)) + { + throw new ArgumentOutOfRangeException(nameof(to)); + } + if (from.HasValue && to.HasValue && (from.Value > to.Value)) + { + throw new ArgumentOutOfRangeException(nameof(from)); + } + + _from = from; + _to = to; + } + + public long? From + { + get { return _from; } + } + + public long? To + { + get { return _to; } + } + + public override string ToString() + { + if (!_from.HasValue) + { + return "-" + _to.Value.ToString(NumberFormatInfo.InvariantInfo); + } + else if (!_to.HasValue) + { + return _from.Value.ToString(NumberFormatInfo.InvariantInfo) + "-"; + } + return _from.Value.ToString(NumberFormatInfo.InvariantInfo) + "-" + + _to.Value.ToString(NumberFormatInfo.InvariantInfo); + } + + public override bool Equals(object obj) + { + var other = obj as RangeItemHeaderValue; + + if (other == null) + { + return false; + } + return ((_from == other._from) && (_to == other._to)); + } + + public override int GetHashCode() + { + if (!_from.HasValue) + { + return _to.GetHashCode(); + } + else if (!_to.HasValue) + { + return _from.GetHashCode(); + } + return _from.GetHashCode() ^ _to.GetHashCode(); + } + + // Returns the length of a range list. E.g. "1-2, 3-4, 5-6" adds 3 ranges to 'rangeCollection'. Note that empty + // list segments are allowed, e.g. ",1-2, , 3-4,,". + internal static int GetRangeItemListLength( + StringSegment input, + int startIndex, + ICollection<RangeItemHeaderValue> rangeCollection) + { + Contract.Requires(rangeCollection != null); + Contract.Requires(startIndex >= 0); + Contract.Ensures((Contract.Result<int>() == 0) || (rangeCollection.Count > 0), + "If we can parse the string, then we expect to have at least one range item."); + + if ((StringSegment.IsNullOrEmpty(input)) || (startIndex >= input.Length)) + { + return 0; + } + + // Empty segments are allowed, so skip all delimiter-only segments (e.g. ", ,"). + var separatorFound = false; + var current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, startIndex, true, out separatorFound); + // It's OK if we didn't find leading separator characters. Ignore 'separatorFound'. + + if (current == input.Length) + { + return 0; + } + + RangeItemHeaderValue range = null; + while (true) + { + var rangeLength = GetRangeItemLength(input, current, out range); + + if (rangeLength == 0) + { + return 0; + } + + rangeCollection.Add(range); + + current = current + rangeLength; + current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, current, true, out separatorFound); + + // If the string is not consumed, we must have a delimiter, otherwise the string is not a valid + // range list. + if ((current < input.Length) && !separatorFound) + { + return 0; + } + + if (current == input.Length) + { + return current - startIndex; + } + } + } + + internal static int GetRangeItemLength(StringSegment input, int startIndex, out RangeItemHeaderValue parsedValue) + { + Contract.Requires(startIndex >= 0); + + // This parser parses number ranges: e.g. '1-2', '1-', '-2'. + + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; + } + + // Caller must remove leading whitespaces. If not, we'll return 0. + var current = startIndex; + + // Try parse the first value of a value pair. + var fromStartIndex = current; + var fromLength = HttpRuleParser.GetNumberLength(input, current, false); + + if (fromLength > HttpRuleParser.MaxInt64Digits) + { + return 0; + } + + current = current + fromLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + // Afer the first value, the '-' character must follow. + if ((current == input.Length) || (input[current] != '-')) + { + // We need a '-' character otherwise this can't be a valid range. + return 0; + } + + current++; // skip the '-' character + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + var toStartIndex = current; + var toLength = 0; + + // If we didn't reach the end of the string, try parse the second value of the range. + if (current < input.Length) + { + toLength = HttpRuleParser.GetNumberLength(input, current, false); + + if (toLength > HttpRuleParser.MaxInt64Digits) + { + return 0; + } + + current = current + toLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + } + + if ((fromLength == 0) && (toLength == 0)) + { + return 0; // At least one value must be provided in order to be a valid range. + } + + // Try convert first value to int64 + long from = 0; + if ((fromLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(fromStartIndex, fromLength), out from)) + { + return 0; + } + + // Try convert second value to int64 + long to = 0; + if ((toLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(toStartIndex, toLength), out to)) + { + return 0; + } + + // 'from' must not be greater than 'to' + if ((fromLength > 0) && (toLength > 0) && (from > to)) + { + return 0; + } + + parsedValue = new RangeItemHeaderValue((fromLength == 0 ? (long?)null : (long?)from), + (toLength == 0 ? (long?)null : (long?)to)); + return current - startIndex; + } + } +} diff --git a/src/Http/Headers/src/SameSiteMode.cs b/src/Http/Headers/src/SameSiteMode.cs new file mode 100644 index 0000000000000000000000000000000000000000..1976386c859468e69f85cdf80e7dc68eeecc8428 --- /dev/null +++ b/src/Http/Headers/src/SameSiteMode.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Net.Http.Headers +{ + // RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 + public enum SameSiteMode + { + None = 0, + Lax, + Strict + } +} diff --git a/src/Http/Headers/src/SetCookieHeaderValue.cs b/src/Http/Headers/src/SetCookieHeaderValue.cs new file mode 100644 index 0000000000000000000000000000000000000000..f3477648dee2cb106e2e2b9e5606c55f22994d42 --- /dev/null +++ b/src/Http/Headers/src/SetCookieHeaderValue.cs @@ -0,0 +1,523 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Text; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + // http://tools.ietf.org/html/rfc6265 + public class SetCookieHeaderValue + { + private const string ExpiresToken = "expires"; + private const string MaxAgeToken = "max-age"; + private const string DomainToken = "domain"; + private const string PathToken = "path"; + private const string SecureToken = "secure"; + // RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 + private const string SameSiteToken = "samesite"; + private static readonly string SameSiteLaxToken = SameSiteMode.Lax.ToString().ToLower(); + private static readonly string SameSiteStrictToken = SameSiteMode.Strict.ToString().ToLower(); + private const string HttpOnlyToken = "httponly"; + private const string SeparatorToken = "; "; + private const string EqualsToken = "="; + private const string DefaultPath = "/"; // TODO: Used? + + private static readonly HttpHeaderParser<SetCookieHeaderValue> SingleValueParser + = new GenericHeaderParser<SetCookieHeaderValue>(false, GetSetCookieLength); + private static readonly HttpHeaderParser<SetCookieHeaderValue> MultipleValueParser + = new GenericHeaderParser<SetCookieHeaderValue>(true, GetSetCookieLength); + + private StringSegment _name; + private StringSegment _value; + + private SetCookieHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + + public SetCookieHeaderValue(StringSegment name) + : this(name, StringSegment.Empty) + { + } + + public SetCookieHeaderValue(StringSegment name, StringSegment value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + Name = name; + Value = value; + } + + public StringSegment Name + { + get { return _name; } + set + { + CookieHeaderValue.CheckNameFormat(value, nameof(value)); + _name = value; + } + } + + public StringSegment Value + { + get { return _value; } + set + { + CookieHeaderValue.CheckValueFormat(value, nameof(value)); + _value = value; + } + } + + public DateTimeOffset? Expires { get; set; } + + public TimeSpan? MaxAge { get; set; } + + public StringSegment Domain { get; set; } + + public StringSegment Path { get; set; } + + public bool Secure { get; set; } + + public SameSiteMode SameSite { get; set; } + + public bool HttpOnly { get; set; } + + // name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly + public override string ToString() + { + var length = _name.Length + EqualsToken.Length + _value.Length; + + string expires = null; + string maxAge = null; + string sameSite = null; + + if (Expires.HasValue) + { + expires = HeaderUtilities.FormatDate(Expires.Value); + length += SeparatorToken.Length + ExpiresToken.Length + EqualsToken.Length + expires.Length; + } + + if (MaxAge.HasValue) + { + maxAge = HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.Value.TotalSeconds); + length += SeparatorToken.Length + MaxAgeToken.Length + EqualsToken.Length + maxAge.Length; + } + + if (Domain != null) + { + length += SeparatorToken.Length + DomainToken.Length + EqualsToken.Length + Domain.Length; + } + + if (Path != null) + { + length += SeparatorToken.Length + PathToken.Length + EqualsToken.Length + Path.Length; + } + + if (Secure) + { + length += SeparatorToken.Length + SecureToken.Length; + } + + if (SameSite != SameSiteMode.None) + { + sameSite = SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken; + length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length; + } + + if (HttpOnly) + { + length += SeparatorToken.Length + HttpOnlyToken.Length; + } + + var sb = new InplaceStringBuilder(length); + + sb.Append(_name); + sb.Append(EqualsToken); + sb.Append(_value); + + if (expires != null) + { + AppendSegment(ref sb, ExpiresToken, expires); + } + + if (maxAge != null) + { + AppendSegment(ref sb, MaxAgeToken, maxAge); + } + + if (Domain != null) + { + AppendSegment(ref sb, DomainToken, Domain); + } + + if (Path != null) + { + AppendSegment(ref sb, PathToken, Path); + } + + if (Secure) + { + AppendSegment(ref sb, SecureToken, null); + } + + if (SameSite != SameSiteMode.None) + { + AppendSegment(ref sb, SameSiteToken, sameSite); + } + + if (HttpOnly) + { + AppendSegment(ref sb, HttpOnlyToken, null); + } + + return sb.ToString(); + } + + private static void AppendSegment(ref InplaceStringBuilder builder, StringSegment name, StringSegment value) + { + builder.Append(SeparatorToken); + builder.Append(name); + if (value != null) + { + builder.Append(EqualsToken); + builder.Append(value); + } + } + + /// <summary> + /// Append string representation of this <see cref="SetCookieHeaderValue"/> to given + /// <paramref name="builder"/>. + /// </summary> + /// <param name="builder"> + /// The <see cref="StringBuilder"/> to receive the string representation of this + /// <see cref="SetCookieHeaderValue"/>. + /// </param> + public void AppendToStringBuilder(StringBuilder builder) + { + builder.Append(_name); + builder.Append("="); + builder.Append(_value); + + if (Expires.HasValue) + { + AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value)); + } + + if (MaxAge.HasValue) + { + AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.Value.TotalSeconds)); + } + + if (Domain != null) + { + AppendSegment(builder, DomainToken, Domain); + } + + if (Path != null) + { + AppendSegment(builder, PathToken, Path); + } + + if (Secure) + { + AppendSegment(builder, SecureToken, null); + } + + if (SameSite != SameSiteMode.None) + { + AppendSegment(builder, SameSiteToken, SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken); + } + + if (HttpOnly) + { + AppendSegment(builder, HttpOnlyToken, null); + } + } + + private static void AppendSegment(StringBuilder builder, StringSegment name, StringSegment value) + { + builder.Append("; "); + builder.Append(name); + if (value != null) + { + builder.Append("="); + builder.Append(value); + } + } + + public static SetCookieHeaderValue Parse(StringSegment input) + { + var index = 0; + return SingleValueParser.ParseValue(input, ref index); + } + + public static bool TryParse(StringSegment input, out SetCookieHeaderValue parsedValue) + { + var index = 0; + return SingleValueParser.TryParseValue(input, ref index, out parsedValue); + } + + public static IList<SetCookieHeaderValue> ParseList(IList<string> inputs) + { + return MultipleValueParser.ParseValues(inputs); + } + + public static IList<SetCookieHeaderValue> ParseStrictList(IList<string> inputs) + { + return MultipleValueParser.ParseStrictValues(inputs); + } + + public static bool TryParseList(IList<string> inputs, out IList<SetCookieHeaderValue> parsedValues) + { + return MultipleValueParser.TryParseValues(inputs, out parsedValues); + } + + public static bool TryParseStrictList(IList<string> inputs, out IList<SetCookieHeaderValue> parsedValues) + { + return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); + } + + // name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly + private static int GetSetCookieLength(StringSegment input, int startIndex, out SetCookieHeaderValue parsedValue) + { + Contract.Requires(startIndex >= 0); + var offset = startIndex; + + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length)) + { + return 0; + } + + var result = new SetCookieHeaderValue(); + + // The caller should have already consumed any leading whitespace, commas, etc.. + + // Name=value; + + // Name + var itemLength = HttpRuleParser.GetTokenLength(input, offset); + if (itemLength == 0) + { + return 0; + } + result._name = input.Subsegment(offset, itemLength); + offset += itemLength; + + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) + { + return 0; + } + + // value or "quoted value" + // The value may be empty + result._value = CookieHeaderValue.GetCookieValue(input, ref offset); + + // *(';' SP cookie-av) + while (offset < input.Length) + { + if (input[offset] == ',') + { + // Divider between headers + break; + } + if (input[offset] != ';') + { + // Expecting a ';' between parameters + return 0; + } + offset++; + + offset += HttpRuleParser.GetWhitespaceLength(input, offset); + + // cookie-av = expires-av / max-age-av / domain-av / path-av / secure-av / samesite-av / httponly-av / extension-av + itemLength = HttpRuleParser.GetTokenLength(input, offset); + if (itemLength == 0) + { + // Trailing ';' or leading into garbage. Let the next parser fail. + break; + } + var token = input.Subsegment(offset, itemLength); + offset += itemLength; + + // expires-av = "Expires=" sane-cookie-date + if (StringSegment.Equals(token, ExpiresToken, StringComparison.OrdinalIgnoreCase)) + { + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) + { + return 0; + } + var dateString = ReadToSemicolonOrEnd(input, ref offset); + DateTimeOffset expirationDate; + if (!HttpRuleParser.TryStringToDate(dateString, out expirationDate)) + { + // Invalid expiration date, abort + return 0; + } + result.Expires = expirationDate; + } + // max-age-av = "Max-Age=" non-zero-digit *DIGIT + else if (StringSegment.Equals(token, MaxAgeToken, StringComparison.OrdinalIgnoreCase)) + { + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) + { + return 0; + } + + itemLength = HttpRuleParser.GetNumberLength(input, offset, allowDecimal: false); + if (itemLength == 0) + { + return 0; + } + var numberString = input.Subsegment(offset, itemLength); + long maxAge; + if (!HeaderUtilities.TryParseNonNegativeInt64(numberString, out maxAge)) + { + // Invalid expiration date, abort + return 0; + } + result.MaxAge = TimeSpan.FromSeconds(maxAge); + offset += itemLength; + } + // domain-av = "Domain=" domain-value + // domain-value = <subdomain> ; defined in [RFC1034], Section 3.5, as enhanced by [RFC1123], Section 2.1 + else if (StringSegment.Equals(token, DomainToken, StringComparison.OrdinalIgnoreCase)) + { + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) + { + return 0; + } + // We don't do any detailed validation on the domain. + result.Domain = ReadToSemicolonOrEnd(input, ref offset); + } + // path-av = "Path=" path-value + // path-value = <any CHAR except CTLs or ";"> + else if (StringSegment.Equals(token, PathToken, StringComparison.OrdinalIgnoreCase)) + { + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) + { + return 0; + } + // We don't do any detailed validation on the path. + result.Path = ReadToSemicolonOrEnd(input, ref offset); + } + // secure-av = "Secure" + else if (StringSegment.Equals(token, SecureToken, StringComparison.OrdinalIgnoreCase)) + { + result.Secure = true; + } + // samesite-av = "SameSite" / "SameSite=" samesite-value + // samesite-value = "Strict" / "Lax" + else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase)) + { + if (!ReadEqualsSign(input, ref offset)) + { + result.SameSite = SameSiteMode.Strict; + } + else + { + var enforcementMode = ReadToSemicolonOrEnd(input, ref offset); + + if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase)) + { + result.SameSite = SameSiteMode.Lax; + } + else + { + result.SameSite = SameSiteMode.Strict; + } + } + } + // httponly-av = "HttpOnly" + else if (StringSegment.Equals(token, HttpOnlyToken, StringComparison.OrdinalIgnoreCase)) + { + result.HttpOnly = true; + } + // extension-av = <any CHAR except CTLs or ";"> + else + { + // TODO: skip it? Store it in a list? + } + } + + parsedValue = result; + return offset - startIndex; + } + + private static bool ReadEqualsSign(StringSegment input, ref int offset) + { + // = (no spaces) + if (offset >= input.Length || input[offset] != '=') + { + return false; + } + offset++; + return true; + } + + private static StringSegment ReadToSemicolonOrEnd(StringSegment input, ref int offset) + { + var end = input.IndexOf(';', offset); + if (end < 0) + { + // Remainder of the string + end = input.Length; + } + var itemLength = end - offset; + var result = input.Subsegment(offset, itemLength); + offset += itemLength; + return result; + } + + public override bool Equals(object obj) + { + var other = obj as SetCookieHeaderValue; + + if (other == null) + { + return false; + } + + return StringSegment.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase) + && StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase) + && Expires.Equals(other.Expires) + && MaxAge.Equals(other.MaxAge) + && StringSegment.Equals(Domain, other.Domain, StringComparison.OrdinalIgnoreCase) + && StringSegment.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase) + && Secure == other.Secure + && SameSite == other.SameSite + && HttpOnly == other.HttpOnly; + } + + public override int GetHashCode() + { + return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name) + ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value) + ^ (Expires.HasValue ? Expires.GetHashCode() : 0) + ^ (MaxAge.HasValue ? MaxAge.GetHashCode() : 0) + ^ (Domain != null ? StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(Domain) : 0) + ^ (Path != null ? StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(Path) : 0) + ^ Secure.GetHashCode() + ^ SameSite.GetHashCode() + ^ HttpOnly.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/src/Http/Headers/src/StringWithQualityHeaderValue.cs b/src/Http/Headers/src/StringWithQualityHeaderValue.cs new file mode 100644 index 0000000000000000000000000000000000000000..deba2d26975e61b32f02771629cf2b4e131e596a --- /dev/null +++ b/src/Http/Headers/src/StringWithQualityHeaderValue.cs @@ -0,0 +1,222 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Globalization; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + public class StringWithQualityHeaderValue + { + private static readonly HttpHeaderParser<StringWithQualityHeaderValue> SingleValueParser + = new GenericHeaderParser<StringWithQualityHeaderValue>(false, GetStringWithQualityLength); + private static readonly HttpHeaderParser<StringWithQualityHeaderValue> MultipleValueParser + = new GenericHeaderParser<StringWithQualityHeaderValue>(true, GetStringWithQualityLength); + + private StringSegment _value; + private double? _quality; + + private StringWithQualityHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + + public StringWithQualityHeaderValue(StringSegment value) + { + HeaderUtilities.CheckValidToken(value, nameof(value)); + + _value = value; + } + + public StringWithQualityHeaderValue(StringSegment value, double quality) + { + HeaderUtilities.CheckValidToken(value, nameof(value)); + + if ((quality < 0) || (quality > 1)) + { + throw new ArgumentOutOfRangeException(nameof(quality)); + } + + _value = value; + _quality = quality; + } + + public StringSegment Value + { + get { return _value; } + } + + public double? Quality + { + get { return _quality; } + } + + public override string ToString() + { + if (_quality.HasValue) + { + return _value + "; q=" + _quality.Value.ToString("0.0##", NumberFormatInfo.InvariantInfo); + } + + return _value.ToString(); + } + + public override bool Equals(object obj) + { + var other = obj as StringWithQualityHeaderValue; + + if (other == null) + { + return false; + } + + if (!StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (_quality.HasValue) + { + // Note that we don't consider double.Epsilon here. We really consider two values equal if they're + // actually equal. This makes sure that we also get the same hashcode for two values considered equal + // by Equals(). + return other._quality.HasValue && (_quality.Value == other._quality.Value); + } + + // If we don't have a quality value, then 'other' must also have no quality assigned in order to be + // considered equal. + return !other._quality.HasValue; + } + + public override int GetHashCode() + { + var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value); + + if (_quality.HasValue) + { + result = result ^ _quality.Value.GetHashCode(); + } + + return result; + } + + public static StringWithQualityHeaderValue Parse(StringSegment input) + { + var index = 0; + return SingleValueParser.ParseValue(input, ref index); + } + + public static bool TryParse(StringSegment input, out StringWithQualityHeaderValue parsedValue) + { + var index = 0; + return SingleValueParser.TryParseValue(input, ref index, out parsedValue); + } + + public static IList<StringWithQualityHeaderValue> ParseList(IList<string> input) + { + return MultipleValueParser.ParseValues(input); + } + + public static IList<StringWithQualityHeaderValue> ParseStrictList(IList<string> input) + { + return MultipleValueParser.ParseStrictValues(input); + } + + public static bool TryParseList(IList<string> input, out IList<StringWithQualityHeaderValue> parsedValues) + { + return MultipleValueParser.TryParseValues(input, out parsedValues); + } + + public static bool TryParseStrictList(IList<string> input, out IList<StringWithQualityHeaderValue> parsedValues) + { + return MultipleValueParser.TryParseStrictValues(input, out parsedValues); + } + + private static int GetStringWithQualityLength(StringSegment input, int startIndex, out StringWithQualityHeaderValue parsedValue) + { + Contract.Requires(startIndex >= 0); + + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; + } + + // Parse the value string: <value> in '<value>; q=<quality>' + var valueLength = HttpRuleParser.GetTokenLength(input, startIndex); + + if (valueLength == 0) + { + return 0; + } + + StringWithQualityHeaderValue result = new StringWithQualityHeaderValue(); + result._value = input.Subsegment(startIndex, valueLength); + var current = startIndex + valueLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + if ((current == input.Length) || (input[current] != ';')) + { + parsedValue = result; + return current - startIndex; // we have a valid token, but no quality. + } + + current++; // skip ';' separator + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + // If we found a ';' separator, it must be followed by a quality information + if (!TryReadQuality(input, result, ref current)) + { + return 0; + } + + parsedValue = result; + return current - startIndex; + } + + private static bool TryReadQuality(StringSegment input, StringWithQualityHeaderValue result, ref int index) + { + var current = index; + + // See if we have a quality value by looking for "q" + if ((current == input.Length) || ((input[current] != 'q') && (input[current] != 'Q'))) + { + return false; + } + + current++; // skip 'q' identifier + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + // If we found "q" it must be followed by "=" + if ((current == input.Length) || (input[current] != '=')) + { + return false; + } + + current++; // skip '=' separator + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + if (current == input.Length) + { + return false; + } + + if (!HeaderUtilities.TryParseQualityDouble(input, current, out var quality, out var qualityLength)) + { + return false; + } + + result._quality = quality; + + current = current + qualityLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + index = current; + return true; + } + } +} diff --git a/src/Http/Headers/src/StringWithQualityHeaderValueComparer.cs b/src/Http/Headers/src/StringWithQualityHeaderValueComparer.cs new file mode 100644 index 0000000000000000000000000000000000000000..961cc078417bff23d70f3a7f97439d2ef3604ec4 --- /dev/null +++ b/src/Http/Headers/src/StringWithQualityHeaderValueComparer.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + /// <summary> + /// Implementation of <see cref="IComparer{T}"/> that can compare content negotiation header fields + /// based on their quality values (a.k.a q-values). This applies to values used in accept-charset, + /// accept-encoding, accept-language and related header fields with similar syntax rules. See + /// <see cref="MediaTypeHeaderValueComparer"/> for a comparer for media type + /// q-values. + /// </summary> + public class StringWithQualityHeaderValueComparer : IComparer<StringWithQualityHeaderValue> + { + private static readonly StringWithQualityHeaderValueComparer _qualityComparer = + new StringWithQualityHeaderValueComparer(); + + private StringWithQualityHeaderValueComparer() + { + } + + public static StringWithQualityHeaderValueComparer QualityComparer + { + get { return _qualityComparer; } + } + + /// <summary> + /// Compares two <see cref="StringWithQualityHeaderValue"/> based on their quality value + /// (a.k.a their "q-value"). + /// Values with identical q-values are considered equal (i.e the result is 0) with the exception of wild-card + /// values (i.e. a value of "*") which are considered less than non-wild-card values. This allows to sort + /// a sequence of <see cref="StringWithQualityHeaderValue"/> following their q-values ending up with any + /// wild-cards at the end. + /// </summary> + /// <param name="stringWithQuality1">The first value to compare.</param> + /// <param name="stringWithQuality2">The second value to compare</param> + /// <returns>The result of the comparison.</returns> + public int Compare( + StringWithQualityHeaderValue stringWithQuality1, + StringWithQualityHeaderValue stringWithQuality2) + { + if (stringWithQuality1 == null) + { + throw new ArgumentNullException(nameof(stringWithQuality1)); + } + + if (stringWithQuality2 == null) + { + throw new ArgumentNullException(nameof(stringWithQuality2)); + } + + var quality1 = stringWithQuality1.Quality ?? HeaderQuality.Match; + var quality2 = stringWithQuality2.Quality ?? HeaderQuality.Match; + var qualityDifference = quality1 - quality2; + if (qualityDifference < 0) + { + return -1; + } + else if (qualityDifference > 0) + { + return 1; + } + + if (!StringSegment.Equals(stringWithQuality1.Value, stringWithQuality2.Value, StringComparison.OrdinalIgnoreCase)) + { + if (StringSegment.Equals(stringWithQuality1.Value, "*", StringComparison.Ordinal)) + { + return -1; + } + else if (StringSegment.Equals(stringWithQuality2.Value, "*", StringComparison.Ordinal)) + { + return 1; + } + } + + return 0; + } + } +} diff --git a/src/Http/Headers/src/baseline.netcore.json b/src/Http/Headers/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..476f8150a73c576a60f8f9ec1f6c05727c7188f7 --- /dev/null +++ b/src/Http/Headers/src/baseline.netcore.json @@ -0,0 +1,4110 @@ +{ + "AssemblyIdentity": "Microsoft.Net.Http.Headers, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Net.Http.Headers.CacheControlHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_NoCache", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_NoCache", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_NoCacheHeaders", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection<Microsoft.Extensions.Primitives.StringSegment>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_NoStore", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_NoStore", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxAge", + "Parameters": [], + "ReturnType": "System.Nullable<System.TimeSpan>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.TimeSpan>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SharedMaxAge", + "Parameters": [], + "ReturnType": "System.Nullable<System.TimeSpan>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SharedMaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.TimeSpan>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxStale", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxStale", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxStaleLimit", + "Parameters": [], + "ReturnType": "System.Nullable<System.TimeSpan>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxStaleLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.TimeSpan>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MinFresh", + "Parameters": [], + "ReturnType": "System.Nullable<System.TimeSpan>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MinFresh", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.TimeSpan>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_NoTransform", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_NoTransform", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnlyIfCached", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnlyIfCached", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Public", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Public", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Private", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Private", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PrivateHeaders", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection<Microsoft.Extensions.Primitives.StringSegment>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MustRevalidate", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MustRevalidate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ProxyRevalidate", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProxyRevalidate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Extensions", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.NameValueHeaderValue>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.CacheControlHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.CacheControlHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "PublicString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "PrivateString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "MaxAgeString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "SharedMaxAgeString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "NoCacheString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "NoStoreString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "MaxStaleString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "MinFreshString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "NoTransformString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "OnlyIfCachedString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "MustRevalidateString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "ProxyRevalidateString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_DispositionType", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DispositionType", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Parameters", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.NameValueHeaderValue>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Name", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FileName", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_FileName", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FileNameStar", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_FileNameStar", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CreationDate", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CreationDate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ModificationDate", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ModificationDate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ReadDate", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ReadDate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Size", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Size", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.Int64>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetHttpFileName", + "Parameters": [ + { + "Name": "fileName", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetMimeFileName", + "Parameters": [ + { + "Name": "fileName", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "dispositionType", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValueIdentityExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "IsFileDisposition", + "Parameters": [ + { + "Name": "header", + "Type": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsFormDisposition", + "Parameters": [ + { + "Name": "header", + "Type": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.ContentRangeHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Unit", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Unit", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_From", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_To", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Length", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasLength", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasRange", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.ContentRangeHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.ContentRangeHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "from", + "Type": "System.Int64" + }, + { + "Name": "to", + "Type": "System.Int64" + }, + { + "Name": "length", + "Type": "System.Int64" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "length", + "Type": "System.Int64" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "from", + "Type": "System.Int64" + }, + { + "Name": "to", + "Type": "System.Int64" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.CookieHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Name", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Value", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.CookieHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.CookieHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + } + ], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.CookieHeaderValue>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + } + ], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.CookieHeaderValue>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.CookieHeaderValue>", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.CookieHeaderValue>", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.EntityTagHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Any", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.EntityTagHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Tag", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsWeak", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Compare", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.Net.Http.Headers.EntityTagHeaderValue" + }, + { + "Name": "useStrongComparison", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.EntityTagHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.EntityTagHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + } + ], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.EntityTagHeaderValue>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + } + ], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.EntityTagHeaderValue>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.EntityTagHeaderValue>", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.EntityTagHeaderValue>", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "tag", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "tag", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "isWeak", + "Type": "System.Boolean" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.HeaderNames", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Accept", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Accept\"" + }, + { + "Kind": "Field", + "Name": "AcceptCharset", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Accept-Charset\"" + }, + { + "Kind": "Field", + "Name": "AcceptEncoding", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Accept-Encoding\"" + }, + { + "Kind": "Field", + "Name": "AcceptLanguage", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Accept-Language\"" + }, + { + "Kind": "Field", + "Name": "AcceptRanges", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Accept-Ranges\"" + }, + { + "Kind": "Field", + "Name": "AccessControlAllowCredentials", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Allow-Credentials\"" + }, + { + "Kind": "Field", + "Name": "AccessControlAllowHeaders", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Allow-Headers\"" + }, + { + "Kind": "Field", + "Name": "AccessControlAllowMethods", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Allow-Methods\"" + }, + { + "Kind": "Field", + "Name": "AccessControlAllowOrigin", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Allow-Origin\"" + }, + { + "Kind": "Field", + "Name": "AccessControlExposeHeaders", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Expose-Headers\"" + }, + { + "Kind": "Field", + "Name": "AccessControlMaxAge", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Max-Age\"" + }, + { + "Kind": "Field", + "Name": "AccessControlRequestHeaders", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Request-Headers\"" + }, + { + "Kind": "Field", + "Name": "AccessControlRequestMethod", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Request-Method\"" + }, + { + "Kind": "Field", + "Name": "Age", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Age\"" + }, + { + "Kind": "Field", + "Name": "Allow", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Allow\"" + }, + { + "Kind": "Field", + "Name": "Authority", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\":authority\"" + }, + { + "Kind": "Field", + "Name": "Authorization", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Authorization\"" + }, + { + "Kind": "Field", + "Name": "CacheControl", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Cache-Control\"" + }, + { + "Kind": "Field", + "Name": "Connection", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Connection\"" + }, + { + "Kind": "Field", + "Name": "ContentDisposition", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Disposition\"" + }, + { + "Kind": "Field", + "Name": "ContentEncoding", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Encoding\"" + }, + { + "Kind": "Field", + "Name": "ContentLanguage", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Language\"" + }, + { + "Kind": "Field", + "Name": "ContentLength", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Length\"" + }, + { + "Kind": "Field", + "Name": "ContentLocation", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Location\"" + }, + { + "Kind": "Field", + "Name": "ContentMD5", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-MD5\"" + }, + { + "Kind": "Field", + "Name": "ContentRange", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Range\"" + }, + { + "Kind": "Field", + "Name": "ContentSecurityPolicy", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Security-Policy\"" + }, + { + "Kind": "Field", + "Name": "ContentSecurityPolicyReportOnly", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Security-Policy-Report-Only\"" + }, + { + "Kind": "Field", + "Name": "ContentType", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Type\"" + }, + { + "Kind": "Field", + "Name": "Cookie", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Cookie\"" + }, + { + "Kind": "Field", + "Name": "Date", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Date\"" + }, + { + "Kind": "Field", + "Name": "ETag", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"ETag\"" + }, + { + "Kind": "Field", + "Name": "Expires", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Expires\"" + }, + { + "Kind": "Field", + "Name": "Expect", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Expect\"" + }, + { + "Kind": "Field", + "Name": "From", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"From\"" + }, + { + "Kind": "Field", + "Name": "Host", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Host\"" + }, + { + "Kind": "Field", + "Name": "IfMatch", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"If-Match\"" + }, + { + "Kind": "Field", + "Name": "IfModifiedSince", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"If-Modified-Since\"" + }, + { + "Kind": "Field", + "Name": "IfNoneMatch", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"If-None-Match\"" + }, + { + "Kind": "Field", + "Name": "IfRange", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"If-Range\"" + }, + { + "Kind": "Field", + "Name": "IfUnmodifiedSince", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"If-Unmodified-Since\"" + }, + { + "Kind": "Field", + "Name": "LastModified", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Last-Modified\"" + }, + { + "Kind": "Field", + "Name": "Location", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Location\"" + }, + { + "Kind": "Field", + "Name": "MaxForwards", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Max-Forwards\"" + }, + { + "Kind": "Field", + "Name": "Method", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\":method\"" + }, + { + "Kind": "Field", + "Name": "Origin", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Origin\"" + }, + { + "Kind": "Field", + "Name": "Path", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\":path\"" + }, + { + "Kind": "Field", + "Name": "Pragma", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Pragma\"" + }, + { + "Kind": "Field", + "Name": "ProxyAuthenticate", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Proxy-Authenticate\"" + }, + { + "Kind": "Field", + "Name": "ProxyAuthorization", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Proxy-Authorization\"" + }, + { + "Kind": "Field", + "Name": "Range", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Range\"" + }, + { + "Kind": "Field", + "Name": "Referer", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Referer\"" + }, + { + "Kind": "Field", + "Name": "RetryAfter", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Retry-After\"" + }, + { + "Kind": "Field", + "Name": "Scheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\":scheme\"" + }, + { + "Kind": "Field", + "Name": "Server", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Server\"" + }, + { + "Kind": "Field", + "Name": "SetCookie", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Set-Cookie\"" + }, + { + "Kind": "Field", + "Name": "Status", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\":status\"" + }, + { + "Kind": "Field", + "Name": "StrictTransportSecurity", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Strict-Transport-Security\"" + }, + { + "Kind": "Field", + "Name": "TE", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"TE\"" + }, + { + "Kind": "Field", + "Name": "Trailer", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Trailer\"" + }, + { + "Kind": "Field", + "Name": "TransferEncoding", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Transfer-Encoding\"" + }, + { + "Kind": "Field", + "Name": "Upgrade", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Upgrade\"" + }, + { + "Kind": "Field", + "Name": "UserAgent", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"User-Agent\"" + }, + { + "Kind": "Field", + "Name": "Vary", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Vary\"" + }, + { + "Kind": "Field", + "Name": "Via", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Via\"" + }, + { + "Kind": "Field", + "Name": "Warning", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Warning\"" + }, + { + "Kind": "Field", + "Name": "WebSocketSubProtocols", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Sec-WebSocket-Protocol\"" + }, + { + "Kind": "Field", + "Name": "WWWAuthenticate", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"WWW-Authenticate\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.HeaderQuality", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Match", + "Parameters": [], + "ReturnType": "System.Double", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "1" + }, + { + "Kind": "Field", + "Name": "NoMatch", + "Parameters": [], + "ReturnType": "System.Double", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "0" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.HeaderUtilities", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "TryParseSeconds", + "Parameters": [ + { + "Name": "headerValues", + "Type": "Microsoft.Extensions.Primitives.StringValues" + }, + { + "Name": "targetValue", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Nullable<System.TimeSpan>", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ContainsCacheDirective", + "Parameters": [ + { + "Name": "cacheControlDirectives", + "Type": "Microsoft.Extensions.Primitives.StringValues" + }, + { + "Name": "targetDirectives", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseNonNegativeInt32", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "result", + "Type": "System.Int32", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseNonNegativeInt64", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "result", + "Type": "System.Int64", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FormatNonNegativeInt64", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseDate", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "result", + "Type": "System.DateTimeOffset", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FormatDate", + "Parameters": [ + { + "Name": "dateTime", + "Type": "System.DateTimeOffset" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FormatDate", + "Parameters": [ + { + "Name": "dateTime", + "Type": "System.DateTimeOffset" + }, + { + "Name": "quoted", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RemoveQuotes", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsQuoted", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UnescapeAsQuotedString", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EscapeAsQuotedString", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Charset", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Charset", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Encoding", + "Parameters": [], + "ReturnType": "System.Text.Encoding", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Encoding", + "Parameters": [ + { + "Name": "value", + "Type": "System.Text.Encoding" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Boundary", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Boundary", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Parameters", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.NameValueHeaderValue>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Quality", + "Parameters": [], + "ReturnType": "System.Nullable<System.Double>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Quality", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.Double>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MediaType", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MediaType", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Type", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SubType", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SubTypeWithoutSuffix", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Suffix", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Facets", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable<Microsoft.Extensions.Primitives.StringSegment>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MatchesAllTypes", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MatchesAllSubTypes", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MatchesAllSubTypesWithoutSuffix", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsReadOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsSubsetOf", + "Parameters": [ + { + "Name": "otherMediaType", + "Type": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Copy", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CopyAsReadOnly", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + } + ], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.MediaTypeHeaderValue>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + } + ], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.MediaTypeHeaderValue>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.MediaTypeHeaderValue>", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.MediaTypeHeaderValue>", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "mediaType", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "mediaType", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "quality", + "Type": "System.Double" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.MediaTypeHeaderValueComparer", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.Collections.Generic.IComparer<Microsoft.Net.Http.Headers.MediaTypeHeaderValue>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_QualityComparer", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.MediaTypeHeaderValueComparer", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Compare", + "Parameters": [ + { + "Name": "mediaType1", + "Type": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue" + }, + { + "Name": "mediaType2", + "Type": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue" + } + ], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IComparer<Microsoft.Net.Http.Headers.MediaTypeHeaderValue>", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.NameValueHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Value", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsReadOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Copy", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.NameValueHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CopyAsReadOnly", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.NameValueHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetUnescapedValue", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetAndEscapeValue", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.NameValueHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.NameValueHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList<System.String>" + } + ], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.NameValueHeaderValue>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseStrictList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList<System.String>" + } + ], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.NameValueHeaderValue>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList<System.String>" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.NameValueHeaderValue>", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseStrictList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList<System.String>" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.NameValueHeaderValue>", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Find", + "Parameters": [ + { + "Name": "values", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.NameValueHeaderValue>" + }, + { + "Name": "name", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.NameValueHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.RangeConditionHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_LastModified", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_EntityTag", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.EntityTagHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.RangeConditionHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.RangeConditionHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "lastModified", + "Type": "System.DateTimeOffset" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "entityTag", + "Type": "Microsoft.Net.Http.Headers.EntityTagHeaderValue" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "entityTag", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.RangeHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Unit", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Unit", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Ranges", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection<Microsoft.Net.Http.Headers.RangeItemHeaderValue>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.RangeHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.RangeHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "from", + "Type": "System.Nullable<System.Int64>" + }, + { + "Name": "to", + "Type": "System.Nullable<System.Int64>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.RangeItemHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_From", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_To", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "from", + "Type": "System.Nullable<System.Int64>" + }, + { + "Name": "to", + "Type": "System.Nullable<System.Int64>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.SameSiteMode", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "None", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "Lax", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + }, + { + "Kind": "Field", + "Name": "Strict", + "Parameters": [], + "GenericParameter": [], + "Literal": "2" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.SetCookieHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Name", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Value", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Expires", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Expires", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxAge", + "Parameters": [], + "ReturnType": "System.Nullable<System.TimeSpan>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.TimeSpan>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Domain", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Domain", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Path", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Secure", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Secure", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SameSite", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.SameSiteMode", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SameSite", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.SameSiteMode" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HttpOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HttpOnly", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendToStringBuilder", + "Parameters": [ + { + "Name": "builder", + "Type": "System.Text.StringBuilder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.SetCookieHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.SetCookieHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + } + ], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.SetCookieHeaderValue>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + } + ], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.SetCookieHeaderValue>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.SetCookieHeaderValue>", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList<System.String>" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.SetCookieHeaderValue>", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.StringWithQualityHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Quality", + "Parameters": [], + "ReturnType": "System.Nullable<System.Double>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.StringWithQualityHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.StringWithQualityHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList<System.String>" + } + ], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.StringWithQualityHeaderValue>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseStrictList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList<System.String>" + } + ], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.StringWithQualityHeaderValue>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList<System.String>" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.StringWithQualityHeaderValue>", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseStrictList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList<System.String>" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.StringWithQualityHeaderValue>", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "quality", + "Type": "System.Double" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.StringWithQualityHeaderValueComparer", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.Collections.Generic.IComparer<Microsoft.Net.Http.Headers.StringWithQualityHeaderValue>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_QualityComparer", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.StringWithQualityHeaderValueComparer", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Compare", + "Parameters": [ + { + "Name": "stringWithQuality1", + "Type": "Microsoft.Net.Http.Headers.StringWithQualityHeaderValue" + }, + { + "Name": "stringWithQuality2", + "Type": "Microsoft.Net.Http.Headers.StringWithQualityHeaderValue" + } + ], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IComparer<Microsoft.Net.Http.Headers.StringWithQualityHeaderValue>", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/Headers/test/CacheControlHeaderValueTest.cs b/src/Http/Headers/test/CacheControlHeaderValueTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..51e8ce5f5805e6b8389c7efc74e9b32b7d56affc --- /dev/null +++ b/src/Http/Headers/test/CacheControlHeaderValueTest.cs @@ -0,0 +1,599 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class CacheControlHeaderValueTest + { + [Fact] + public void Properties_SetAndGetAllProperties_SetValueReturnedInGetter() + { + var cacheControl = new CacheControlHeaderValue(); + + // Bool properties + cacheControl.NoCache = true; + Assert.True(cacheControl.NoCache, "NoCache"); + cacheControl.NoStore = true; + Assert.True(cacheControl.NoStore, "NoStore"); + cacheControl.MaxStale = true; + Assert.True(cacheControl.MaxStale, "MaxStale"); + cacheControl.NoTransform = true; + Assert.True(cacheControl.NoTransform, "NoTransform"); + cacheControl.OnlyIfCached = true; + Assert.True(cacheControl.OnlyIfCached, "OnlyIfCached"); + cacheControl.Public = true; + Assert.True(cacheControl.Public, "Public"); + cacheControl.Private = true; + Assert.True(cacheControl.Private, "Private"); + cacheControl.MustRevalidate = true; + Assert.True(cacheControl.MustRevalidate, "MustRevalidate"); + cacheControl.ProxyRevalidate = true; + Assert.True(cacheControl.ProxyRevalidate, "ProxyRevalidate"); + + // TimeSpan properties + TimeSpan timeSpan = new TimeSpan(1, 2, 3); + cacheControl.MaxAge = timeSpan; + Assert.Equal(timeSpan, cacheControl.MaxAge); + cacheControl.SharedMaxAge = timeSpan; + Assert.Equal(timeSpan, cacheControl.SharedMaxAge); + cacheControl.MaxStaleLimit = timeSpan; + Assert.Equal(timeSpan, cacheControl.MaxStaleLimit); + cacheControl.MinFresh = timeSpan; + Assert.Equal(timeSpan, cacheControl.MinFresh); + + // String collection properties + Assert.NotNull(cacheControl.NoCacheHeaders); + Assert.Throws<ArgumentException>(() => cacheControl.NoCacheHeaders.Add(null)); + Assert.Throws<FormatException>(() => cacheControl.NoCacheHeaders.Add("invalid token")); + cacheControl.NoCacheHeaders.Add("token"); + Assert.Equal(1, cacheControl.NoCacheHeaders.Count); + Assert.Equal("token", cacheControl.NoCacheHeaders.First()); + + Assert.NotNull(cacheControl.PrivateHeaders); + Assert.Throws<ArgumentException>(() => cacheControl.PrivateHeaders.Add(null)); + Assert.Throws<FormatException>(() => cacheControl.PrivateHeaders.Add("invalid token")); + cacheControl.PrivateHeaders.Add("token"); + Assert.Equal(1, cacheControl.PrivateHeaders.Count); + Assert.Equal("token", cacheControl.PrivateHeaders.First()); + + // NameValueHeaderValue collection property + Assert.NotNull(cacheControl.Extensions); + Assert.Throws<ArgumentNullException>(() => cacheControl.Extensions.Add(null)); + cacheControl.Extensions.Add(new NameValueHeaderValue("name", "value")); + Assert.Equal(1, cacheControl.Extensions.Count); + Assert.Equal(new NameValueHeaderValue("name", "value"), cacheControl.Extensions.First()); + } + + [Fact] + public void ToString_UseRequestDirectiveValues_AllSerializedCorrectly() + { + var cacheControl = new CacheControlHeaderValue(); + Assert.Equal("", cacheControl.ToString()); + + // Note that we allow all combinations of all properties even though the RFC specifies rules what value + // can be used together. + // Also for property pairs (bool property + collection property) like 'NoCache' and 'NoCacheHeaders' the + // caller needs to set the bool property in order for the collection to be populated as string. + + // Cache Request Directive sample + cacheControl.NoStore = true; + Assert.Equal("no-store", cacheControl.ToString()); + cacheControl.NoCache = true; + Assert.Equal("no-store, no-cache", cacheControl.ToString()); + cacheControl.MaxAge = new TimeSpan(0, 1, 10); + Assert.Equal("no-store, no-cache, max-age=70", cacheControl.ToString()); + cacheControl.MaxStale = true; + Assert.Equal("no-store, no-cache, max-age=70, max-stale", cacheControl.ToString()); + cacheControl.MaxStaleLimit = new TimeSpan(0, 2, 5); + Assert.Equal("no-store, no-cache, max-age=70, max-stale=125", cacheControl.ToString()); + cacheControl.MinFresh = new TimeSpan(0, 3, 0); + Assert.Equal("no-store, no-cache, max-age=70, max-stale=125, min-fresh=180", cacheControl.ToString()); + + cacheControl = new CacheControlHeaderValue(); + cacheControl.NoTransform = true; + Assert.Equal("no-transform", cacheControl.ToString()); + cacheControl.OnlyIfCached = true; + Assert.Equal("no-transform, only-if-cached", cacheControl.ToString()); + cacheControl.Extensions.Add(new NameValueHeaderValue("custom")); + cacheControl.Extensions.Add(new NameValueHeaderValue("customName", "customValue")); + Assert.Equal("no-transform, only-if-cached, custom, customName=customValue", cacheControl.ToString()); + + cacheControl = new CacheControlHeaderValue(); + cacheControl.Extensions.Add(new NameValueHeaderValue("custom")); + Assert.Equal("custom", cacheControl.ToString()); + } + + [Fact] + public void ToString_UseResponseDirectiveValues_AllSerializedCorrectly() + { + var cacheControl = new CacheControlHeaderValue(); + Assert.Equal("", cacheControl.ToString()); + + cacheControl.NoCache = true; + Assert.Equal("no-cache", cacheControl.ToString()); + cacheControl.NoCacheHeaders.Add("token1"); + Assert.Equal("no-cache=\"token1\"", cacheControl.ToString()); + cacheControl.Public = true; + Assert.Equal("public, no-cache=\"token1\"", cacheControl.ToString()); + + cacheControl = new CacheControlHeaderValue(); + cacheControl.Private = true; + Assert.Equal("private", cacheControl.ToString()); + cacheControl.PrivateHeaders.Add("token2"); + cacheControl.PrivateHeaders.Add("token3"); + Assert.Equal("private=\"token2, token3\"", cacheControl.ToString()); + cacheControl.MustRevalidate = true; + Assert.Equal("must-revalidate, private=\"token2, token3\"", cacheControl.ToString()); + cacheControl.ProxyRevalidate = true; + Assert.Equal("must-revalidate, proxy-revalidate, private=\"token2, token3\"", cacheControl.ToString()); + } + + [Fact] + public void GetHashCode_CompareValuesWithBoolFieldsSet_MatchExpectation() + { + // Verify that different bool fields return different hash values. + var values = new CacheControlHeaderValue[9]; + + for (int i = 0; i < values.Length; i++) + { + values[i] = new CacheControlHeaderValue(); + } + + values[0].ProxyRevalidate = true; + values[1].NoCache = true; + values[2].NoStore = true; + values[3].MaxStale = true; + values[4].NoTransform = true; + values[5].OnlyIfCached = true; + values[6].Public = true; + values[7].Private = true; + values[8].MustRevalidate = true; + + // Only one bool field set. All hash codes should differ + for (int i = 0; i < values.Length; i++) + { + for (int j = 0; j < values.Length; j++) + { + if (i != j) + { + CompareHashCodes(values[i], values[j], false); + } + } + } + + // Validate that two instances with the same bool fields set are equal. + values[0].NoCache = true; + CompareHashCodes(values[0], values[1], false); + values[1].ProxyRevalidate = true; + CompareHashCodes(values[0], values[1], true); + } + + [Fact] + public void GetHashCode_CompareValuesWithTimeSpanFieldsSet_MatchExpectation() + { + // Verify that different timespan fields return different hash values. + var values = new CacheControlHeaderValue[4]; + + for (int i = 0; i < values.Length; i++) + { + values[i] = new CacheControlHeaderValue(); + } + + values[0].MaxAge = new TimeSpan(0, 1, 1); + values[1].MaxStaleLimit = new TimeSpan(0, 1, 1); + values[2].MinFresh = new TimeSpan(0, 1, 1); + values[3].SharedMaxAge = new TimeSpan(0, 1, 1); + + // Only one timespan field set. All hash codes should differ + for (int i = 0; i < values.Length; i++) + { + for (int j = 0; j < values.Length; j++) + { + if (i != j) + { + CompareHashCodes(values[i], values[j], false); + } + } + } + + values[0].MaxStaleLimit = new TimeSpan(0, 1, 2); + CompareHashCodes(values[0], values[1], false); + + values[1].MaxAge = new TimeSpan(0, 1, 1); + values[1].MaxStaleLimit = new TimeSpan(0, 1, 2); + CompareHashCodes(values[0], values[1], true); + } + + [Fact] + public void GetHashCode_CompareCollectionFieldsSet_MatchExpectation() + { + var cacheControl1 = new CacheControlHeaderValue(); + var cacheControl2 = new CacheControlHeaderValue(); + var cacheControl3 = new CacheControlHeaderValue(); + var cacheControl4 = new CacheControlHeaderValue(); + var cacheControl5 = new CacheControlHeaderValue(); + + cacheControl1.NoCache = true; + cacheControl1.NoCacheHeaders.Add("token2"); + + cacheControl2.NoCache = true; + cacheControl2.NoCacheHeaders.Add("token1"); + cacheControl2.NoCacheHeaders.Add("token2"); + + CompareHashCodes(cacheControl1, cacheControl2, false); + + cacheControl1.NoCacheHeaders.Add("token1"); + CompareHashCodes(cacheControl1, cacheControl2, true); + + // Since NoCache and Private generate different hash codes, even if NoCacheHeaders and PrivateHeaders + // have the same values, the hash code will be different. + cacheControl3.Private = true; + cacheControl3.PrivateHeaders.Add("token2"); + CompareHashCodes(cacheControl1, cacheControl3, false); + + + cacheControl4.Extensions.Add(new NameValueHeaderValue("custom")); + CompareHashCodes(cacheControl1, cacheControl4, false); + + cacheControl5.Extensions.Add(new NameValueHeaderValue("customN", "customV")); + cacheControl5.Extensions.Add(new NameValueHeaderValue("custom")); + CompareHashCodes(cacheControl4, cacheControl5, false); + + cacheControl4.Extensions.Add(new NameValueHeaderValue("customN", "customV")); + CompareHashCodes(cacheControl4, cacheControl5, true); + } + + [Fact] + public void Equals_CompareValuesWithBoolFieldsSet_MatchExpectation() + { + // Verify that different bool fields return different hash values. + var values = new CacheControlHeaderValue[9]; + + for (int i = 0; i < values.Length; i++) + { + values[i] = new CacheControlHeaderValue(); + } + + values[0].ProxyRevalidate = true; + values[1].NoCache = true; + values[2].NoStore = true; + values[3].MaxStale = true; + values[4].NoTransform = true; + values[5].OnlyIfCached = true; + values[6].Public = true; + values[7].Private = true; + values[8].MustRevalidate = true; + + // Only one bool field set. All hash codes should differ + for (int i = 0; i < values.Length; i++) + { + for (int j = 0; j < values.Length; j++) + { + if (i != j) + { + CompareValues(values[i], values[j], false); + } + } + } + + // Validate that two instances with the same bool fields set are equal. + values[0].NoCache = true; + CompareValues(values[0], values[1], false); + values[1].ProxyRevalidate = true; + CompareValues(values[0], values[1], true); + } + + [Fact] + public void Equals_CompareValuesWithTimeSpanFieldsSet_MatchExpectation() + { + // Verify that different timespan fields return different hash values. + var values = new CacheControlHeaderValue[4]; + + for (int i = 0; i < values.Length; i++) + { + values[i] = new CacheControlHeaderValue(); + } + + values[0].MaxAge = new TimeSpan(0, 1, 1); + values[1].MaxStaleLimit = new TimeSpan(0, 1, 1); + values[2].MinFresh = new TimeSpan(0, 1, 1); + values[3].SharedMaxAge = new TimeSpan(0, 1, 1); + + // Only one timespan field set. All hash codes should differ + for (int i = 0; i < values.Length; i++) + { + for (int j = 0; j < values.Length; j++) + { + if (i != j) + { + CompareValues(values[i], values[j], false); + } + } + } + + values[0].MaxStaleLimit = new TimeSpan(0, 1, 2); + CompareValues(values[0], values[1], false); + + values[1].MaxAge = new TimeSpan(0, 1, 1); + values[1].MaxStaleLimit = new TimeSpan(0, 1, 2); + CompareValues(values[0], values[1], true); + + var value1 = new CacheControlHeaderValue(); + value1.MaxStale = true; + var value2 = new CacheControlHeaderValue(); + value2.MaxStale = true; + CompareValues(value1, value2, true); + + value2.MaxStaleLimit = new TimeSpan(1, 2, 3); + CompareValues(value1, value2, false); + } + + [Fact] + public void Equals_CompareCollectionFieldsSet_MatchExpectation() + { + var cacheControl1 = new CacheControlHeaderValue(); + var cacheControl2 = new CacheControlHeaderValue(); + var cacheControl3 = new CacheControlHeaderValue(); + var cacheControl4 = new CacheControlHeaderValue(); + var cacheControl5 = new CacheControlHeaderValue(); + var cacheControl6 = new CacheControlHeaderValue(); + + cacheControl1.NoCache = true; + cacheControl1.NoCacheHeaders.Add("token2"); + + Assert.False(cacheControl1.Equals(null), "Compare with 'null'"); + + cacheControl2.NoCache = true; + cacheControl2.NoCacheHeaders.Add("token1"); + cacheControl2.NoCacheHeaders.Add("token2"); + + CompareValues(cacheControl1, cacheControl2, false); + + cacheControl1.NoCacheHeaders.Add("token1"); + CompareValues(cacheControl1, cacheControl2, true); + + // Since NoCache and Private generate different hash codes, even if NoCacheHeaders and PrivateHeaders + // have the same values, the hash code will be different. + cacheControl3.Private = true; + cacheControl3.PrivateHeaders.Add("token2"); + CompareValues(cacheControl1, cacheControl3, false); + + cacheControl4.Private = true; + cacheControl4.PrivateHeaders.Add("token3"); + CompareValues(cacheControl3, cacheControl4, false); + + cacheControl5.Extensions.Add(new NameValueHeaderValue("custom")); + CompareValues(cacheControl1, cacheControl5, false); + + cacheControl6.Extensions.Add(new NameValueHeaderValue("customN", "customV")); + cacheControl6.Extensions.Add(new NameValueHeaderValue("custom")); + CompareValues(cacheControl5, cacheControl6, false); + + cacheControl5.Extensions.Add(new NameValueHeaderValue("customN", "customV")); + CompareValues(cacheControl5, cacheControl6, true); + } + + [Fact] + public void TryParse_DifferentValidScenarios_AllReturnTrue() + { + var expected = new CacheControlHeaderValue(); + expected.NoCache = true; + CheckValidTryParse(" , no-cache ,,", expected); + + expected = new CacheControlHeaderValue(); + expected.NoCache = true; + expected.NoCacheHeaders.Add("token1"); + expected.NoCacheHeaders.Add("token2"); + CheckValidTryParse("no-cache=\"token1, token2\"", expected); + + expected = new CacheControlHeaderValue(); + expected.NoStore = true; + expected.MaxAge = new TimeSpan(0, 0, 125); + expected.MaxStale = true; + CheckValidTryParse(" no-store , max-age = 125, max-stale,", expected); + + expected = new CacheControlHeaderValue(); + expected.MinFresh = new TimeSpan(0, 0, 123); + expected.NoTransform = true; + expected.OnlyIfCached = true; + expected.Extensions.Add(new NameValueHeaderValue("custom")); + CheckValidTryParse("min-fresh=123, no-transform, only-if-cached, custom", expected); + + expected = new CacheControlHeaderValue(); + expected.Public = true; + expected.Private = true; + expected.PrivateHeaders.Add("token1"); + expected.MustRevalidate = true; + expected.ProxyRevalidate = true; + expected.Extensions.Add(new NameValueHeaderValue("c", "d")); + expected.Extensions.Add(new NameValueHeaderValue("a", "b")); + CheckValidTryParse(",public, , private=\"token1\", must-revalidate, c=d, proxy-revalidate, a=b", expected); + + expected = new CacheControlHeaderValue(); + expected.Private = true; + expected.SharedMaxAge = new TimeSpan(0, 0, 1234567890); + expected.MaxAge = new TimeSpan(0, 0, 987654321); + CheckValidTryParse("s-maxage=1234567890, private, max-age = 987654321,", expected); + + expected = new CacheControlHeaderValue(); + expected.Extensions.Add(new NameValueHeaderValue("custom", "")); + CheckValidTryParse("custom=", expected); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + // Token-only values + [InlineData("no-store=15")] + [InlineData("no-store=")] + [InlineData("no-transform=a")] + [InlineData("no-transform=")] + [InlineData("only-if-cached=\"x\"")] + [InlineData("only-if-cached=")] + [InlineData("public=\"x\"")] + [InlineData("public=")] + [InlineData("must-revalidate=\"1\"")] + [InlineData("must-revalidate=")] + [InlineData("proxy-revalidate=x")] + [InlineData("proxy-revalidate=")] + // Token with optional field-name list + [InlineData("no-cache=")] + [InlineData("no-cache=token")] + [InlineData("no-cache=\"token")] + [InlineData("no-cache=\"\"")] // at least one token expected as value + [InlineData("private=")] + [InlineData("private=token")] + [InlineData("private=\"token")] + [InlineData("private=\",\"")] // at least one token expected as value + [InlineData("private=\"=\"")] + // Token with delta-seconds value + [InlineData("max-age")] + [InlineData("max-age=")] + [InlineData("max-age=a")] + [InlineData("max-age=\"1\"")] + [InlineData("max-age=1.5")] + [InlineData("max-stale=")] + [InlineData("max-stale=a")] + [InlineData("max-stale=\"1\"")] + [InlineData("max-stale=1.5")] + [InlineData("min-fresh")] + [InlineData("min-fresh=")] + [InlineData("min-fresh=a")] + [InlineData("min-fresh=\"1\"")] + [InlineData("min-fresh=1.5")] + [InlineData("s-maxage")] + [InlineData("s-maxage=")] + [InlineData("s-maxage=a")] + [InlineData("s-maxage=\"1\"")] + [InlineData("s-maxage=1.5")] + // Invalid Extension values + [InlineData("custom value")] + public void TryParse_DifferentInvalidScenarios_ReturnsFalse(string input) + { + CheckInvalidTryParse(input); + } + + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + // Just verify parser is implemented correctly. Don't try to test syntax parsed by CacheControlHeaderValue. + var expected = new CacheControlHeaderValue(); + expected.NoStore = true; + expected.MinFresh = new TimeSpan(0, 2, 3); + CheckValidParse(" , no-store, min-fresh=123", expected); + + expected = new CacheControlHeaderValue(); + expected.MaxStale = true; + expected.NoCache = true; + expected.NoCacheHeaders.Add("t"); + CheckValidParse("max-stale, no-cache=\"t\", ,,", expected); + + expected = new CacheControlHeaderValue(); + expected.Extensions.Add(new NameValueHeaderValue("custom")); + CheckValidParse("custom =", expected); + + expected = new CacheControlHeaderValue(); + expected.Extensions.Add(new NameValueHeaderValue("custom", "")); + CheckValidParse("custom =", expected); + } + + [Fact] + public void Parse_SetOfInvalidValueStrings_Throws() + { + CheckInvalidParse(null); + CheckInvalidParse(""); + CheckInvalidParse(" "); + CheckInvalidParse("no-cache,="); + CheckInvalidParse("max-age=123x"); + CheckInvalidParse("=no-cache"); + CheckInvalidParse("no-cache no-store"); + CheckInvalidParse("会"); + } + + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + // Just verify parser is implemented correctly. Don't try to test syntax parsed by CacheControlHeaderValue. + var expected = new CacheControlHeaderValue(); + expected.NoStore = true; + expected.MinFresh = new TimeSpan(0, 2, 3); + CheckValidTryParse(" , no-store, min-fresh=123", expected); + + expected = new CacheControlHeaderValue(); + expected.MaxStale = true; + expected.NoCache = true; + expected.NoCacheHeaders.Add("t"); + CheckValidTryParse("max-stale, no-cache=\"t\", ,,", expected); + + expected = new CacheControlHeaderValue(); + expected.Extensions.Add(new NameValueHeaderValue("custom")); + CheckValidTryParse("custom = ", expected); + + expected = new CacheControlHeaderValue(); + expected.Extensions.Add(new NameValueHeaderValue("custom", "")); + CheckValidTryParse("custom =", expected); + } + + [Fact] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() + { + CheckInvalidTryParse("no-cache,="); + CheckInvalidTryParse("max-age=123x"); + CheckInvalidTryParse("=no-cache"); + CheckInvalidTryParse("no-cache no-store"); + CheckInvalidTryParse("会"); + } + + #region Helper methods + + private void CompareHashCodes(CacheControlHeaderValue x, CacheControlHeaderValue y, bool areEqual) + { + if (areEqual) + { + Assert.Equal(x.GetHashCode(), y.GetHashCode()); + } + else + { + Assert.NotEqual(x.GetHashCode(), y.GetHashCode()); + } + } + + private void CompareValues(CacheControlHeaderValue x, CacheControlHeaderValue y, bool areEqual) + { + Assert.Equal(areEqual, x.Equals(y)); + Assert.Equal(areEqual, y.Equals(x)); + } + + private void CheckValidParse(string input, CacheControlHeaderValue expectedResult) + { + var result = CacheControlHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidParse(string input) + { + Assert.Throws<FormatException>(() => CacheControlHeaderValue.Parse(input)); + } + + private void CheckValidTryParse(string input, CacheControlHeaderValue expectedResult) + { + CacheControlHeaderValue result = null; + Assert.True(CacheControlHeaderValue.TryParse(input, out result)); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidTryParse(string input) + { + CacheControlHeaderValue result = null; + Assert.False(CacheControlHeaderValue.TryParse(input, out result)); + Assert.Null(result); + } + + #endregion + } +} diff --git a/src/Http/Headers/test/ContentDispositionHeaderValueTest.cs b/src/Http/Headers/test/ContentDispositionHeaderValueTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..ad1f7fce1f4ef6b0df83a97c0ba47f1ab618d877 --- /dev/null +++ b/src/Http/Headers/test/ContentDispositionHeaderValueTest.cs @@ -0,0 +1,622 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class ContentDispositionHeaderValueTest + { + [Fact] + public void Ctor_ContentDispositionNull_Throw() + { + Assert.Throws<ArgumentException>(() => new ContentDispositionHeaderValue(null)); + } + + [Fact] + public void Ctor_ContentDispositionEmpty_Throw() + { + // null and empty should be treated the same. So we also throw for empty strings. + Assert.Throws<ArgumentException>(() => new ContentDispositionHeaderValue(string.Empty)); + } + + [Fact] + public void Ctor_ContentDispositionInvalidFormat_ThrowFormatException() + { + // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. + AssertFormatException(" inline "); + AssertFormatException(" inline"); + AssertFormatException("inline "); + AssertFormatException("\"inline\""); + AssertFormatException("te xt"); + AssertFormatException("te=xt"); + AssertFormatException("teäxt"); + AssertFormatException("text;"); + AssertFormatException("te/xt;"); + AssertFormatException("inline; name=someName; "); + AssertFormatException("text;name=someName"); // ctor takes only disposition-type name, no parameters + } + + [Fact] + public void Ctor_ContentDispositionValidFormat_SuccessfullyCreated() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + Assert.Equal("inline", contentDisposition.DispositionType); + Assert.Equal(0, contentDisposition.Parameters.Count); + Assert.Null(contentDisposition.Name.Value); + Assert.Null(contentDisposition.FileName.Value); + Assert.Null(contentDisposition.CreationDate); + Assert.Null(contentDisposition.ModificationDate); + Assert.Null(contentDisposition.ReadDate); + Assert.Null(contentDisposition.Size); + } + + [Fact] + public void Parameters_AddNull_Throw() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + Assert.Throws<ArgumentNullException>(() => contentDisposition.Parameters.Add(null)); + } + + [Fact] + public void ContentDisposition_SetAndGetContentDisposition_MatchExpectations() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + Assert.Equal("inline", contentDisposition.DispositionType); + + contentDisposition.DispositionType = "attachment"; + Assert.Equal("attachment", contentDisposition.DispositionType); + } + + [Fact] + public void Name_SetNameAndValidateObject_ParametersEntryForNameAdded() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + contentDisposition.Name = "myname"; + Assert.Equal("myname", contentDisposition.Name); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("name", contentDisposition.Parameters.First().Name); + + contentDisposition.Name = null; + Assert.Null(contentDisposition.Name.Value); + Assert.Equal(0, contentDisposition.Parameters.Count); + contentDisposition.Name = null; // It's OK to set it again to null; no exception. + } + + [Fact] + public void Name_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + NameValueHeaderValue name = new NameValueHeaderValue("NAME", "old_name"); + contentDisposition.Parameters.Add(name); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("NAME", contentDisposition.Parameters.First().Name); + + contentDisposition.Name = "new_name"; + Assert.Equal("new_name", contentDisposition.Name); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("NAME", contentDisposition.Parameters.First().Name); + + contentDisposition.Parameters.Remove(name); + Assert.Null(contentDisposition.Name.Value); + } + + [Fact] + public void FileName_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var fileName = new NameValueHeaderValue("FILENAME", "old_name"); + contentDisposition.Parameters.Add(fileName); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name); + + contentDisposition.FileName = "new_name"; + Assert.Equal("new_name", contentDisposition.FileName); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name); + + contentDisposition.Parameters.Remove(fileName); + Assert.Null(contentDisposition.FileName.Value); + } + + [Fact] + public void FileName_NeedsEncoding_EncodedAndDecodedCorrectly() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + contentDisposition.FileName = "FileÃName.bat"; + Assert.Equal("FileÃName.bat", contentDisposition.FileName); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("filename", contentDisposition.Parameters.First().Name); + Assert.Equal("\"=?utf-8?B?RmlsZcODTmFtZS5iYXQ=?=\"", contentDisposition.Parameters.First().Value); + + contentDisposition.Parameters.Remove(contentDisposition.Parameters.First()); + Assert.Null(contentDisposition.FileName.Value); + } + + [Fact] + public void FileName_UnknownOrBadEncoding_PropertyFails() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var fileName = new NameValueHeaderValue("FILENAME", "\"=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=\""); + contentDisposition.Parameters.Add(fileName); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name); + Assert.Equal("\"=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=\"", contentDisposition.Parameters.First().Value); + Assert.Equal("=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=", contentDisposition.FileName); + + contentDisposition.FileName = "new_name"; + Assert.Equal("new_name", contentDisposition.FileName); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name); + + contentDisposition.Parameters.Remove(fileName); + Assert.Null(contentDisposition.FileName.Value); + } + + [Fact] + public void FileNameStar_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var fileNameStar = new NameValueHeaderValue("FILENAME*", "old_name"); + contentDisposition.Parameters.Add(fileNameStar); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); + Assert.Null(contentDisposition.FileNameStar.Value); // Decode failure + + contentDisposition.FileNameStar = "new_name"; + Assert.Equal("new_name", contentDisposition.FileNameStar); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); + Assert.Equal("UTF-8\'\'new_name", contentDisposition.Parameters.First().Value); + + contentDisposition.Parameters.Remove(fileNameStar); + Assert.Null(contentDisposition.FileNameStar.Value); + } + + [Fact] + public void FileNameStar_NeedsEncoding_EncodedAndDecodedCorrectly() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + contentDisposition.FileNameStar = "FileÃName.bat"; + Assert.Equal("FileÃName.bat", contentDisposition.FileNameStar); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("filename*", contentDisposition.Parameters.First().Name); + Assert.Equal("UTF-8\'\'File%C3%83Name.bat", contentDisposition.Parameters.First().Value); + + contentDisposition.Parameters.Remove(contentDisposition.Parameters.First()); + Assert.Null(contentDisposition.FileNameStar.Value); + } + + [Fact] + public void FileNameStar_UnknownOrBadEncoding_PropertyFails() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var fileNameStar = new NameValueHeaderValue("FILENAME*", "utf-99'lang'File%CZName.bat"); + contentDisposition.Parameters.Add(fileNameStar); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); + Assert.Equal("utf-99'lang'File%CZName.bat", contentDisposition.Parameters.First().Value); + Assert.Null(contentDisposition.FileNameStar.Value); // Decode failure + + contentDisposition.FileNameStar = "new_name"; + Assert.Equal("new_name", contentDisposition.FileNameStar); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); + + contentDisposition.Parameters.Remove(fileNameStar); + Assert.Null(contentDisposition.FileNameStar.Value); + } + + [Fact] + public void Dates_AddDateParameterThenUseProperty_ParametersEntryIsOverwritten() + { + string validDateString = "\"Tue, 15 Nov 1994 08:12:31 GMT\""; + DateTimeOffset validDate = DateTimeOffset.Parse("Tue, 15 Nov 1994 08:12:31 GMT"); + + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var dateParameter = new NameValueHeaderValue("Creation-DATE", validDateString); + contentDisposition.Parameters.Add(dateParameter); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("Creation-DATE", contentDisposition.Parameters.First().Name); + + Assert.Equal(validDate, contentDisposition.CreationDate); + + var newDate = validDate.AddSeconds(1); + contentDisposition.CreationDate = newDate; + Assert.Equal(newDate, contentDisposition.CreationDate); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("Creation-DATE", contentDisposition.Parameters.First().Name); + Assert.Equal("\"Tue, 15 Nov 1994 08:12:32 GMT\"", contentDisposition.Parameters.First().Value); + + contentDisposition.Parameters.Remove(dateParameter); + Assert.Null(contentDisposition.CreationDate); + } + + [Fact] + public void Dates_InvalidDates_PropertyFails() + { + string invalidDateString = "\"Tue, 15 Nov 94 08:12 GMT\""; + + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var dateParameter = new NameValueHeaderValue("read-DATE", invalidDateString); + contentDisposition.Parameters.Add(dateParameter); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("read-DATE", contentDisposition.Parameters.First().Name); + + Assert.Null(contentDisposition.ReadDate); + + contentDisposition.ReadDate = null; + Assert.Null(contentDisposition.ReadDate); + Assert.Equal(0, contentDisposition.Parameters.Count); + } + + [Fact] + public void Size_AddSizeParameterThenUseProperty_ParametersEntryIsOverwritten() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var sizeParameter = new NameValueHeaderValue("SIZE", "279172874239"); + contentDisposition.Parameters.Add(sizeParameter); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("SIZE", contentDisposition.Parameters.First().Name); + Assert.Equal(279172874239, contentDisposition.Size); + + contentDisposition.Size = 279172874240; + Assert.Equal(279172874240, contentDisposition.Size); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("SIZE", contentDisposition.Parameters.First().Name); + + contentDisposition.Parameters.Remove(sizeParameter); + Assert.Null(contentDisposition.Size); + } + + [Fact] + public void Size_InvalidSizes_PropertyFails() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var sizeParameter = new NameValueHeaderValue("SIZE", "-279172874239"); + contentDisposition.Parameters.Add(sizeParameter); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("SIZE", contentDisposition.Parameters.First().Name); + Assert.Null(contentDisposition.Size); + + // Negatives not allowed + Assert.Throws<ArgumentOutOfRangeException>(() => contentDisposition.Size = -279172874240); + Assert.Null(contentDisposition.Size); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("SIZE", contentDisposition.Parameters.First().Name); + + contentDisposition.Parameters.Remove(sizeParameter); + Assert.Null(contentDisposition.Size); + } + + [Fact] + public void ToString_UseDifferentContentDispositions_AllSerializedCorrectly() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + Assert.Equal("inline", contentDisposition.ToString()); + + contentDisposition.Name = "myname"; + Assert.Equal("inline; name=myname", contentDisposition.ToString()); + + contentDisposition.FileName = "my File Name"; + Assert.Equal("inline; name=myname; filename=\"my File Name\"", contentDisposition.ToString()); + + contentDisposition.CreationDate = new DateTimeOffset(new DateTime(2011, 2, 15), new TimeSpan(-8, 0, 0)); + Assert.Equal("inline; name=myname; filename=\"my File Name\"; creation-date=" + + "\"Tue, 15 Feb 2011 08:00:00 GMT\"", contentDisposition.ToString()); + + contentDisposition.Parameters.Add(new NameValueHeaderValue("custom", "\"custom value\"")); + Assert.Equal("inline; name=myname; filename=\"my File Name\"; creation-date=" + + "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"", contentDisposition.ToString()); + + contentDisposition.Name = null; + Assert.Equal("inline; filename=\"my File Name\"; creation-date=" + + "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"", contentDisposition.ToString()); + + contentDisposition.FileNameStar = "File%Name"; + Assert.Equal("inline; filename=\"my File Name\"; creation-date=" + + "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"; filename*=UTF-8\'\'File%25Name", + contentDisposition.ToString()); + + contentDisposition.FileName = null; + Assert.Equal("inline; creation-date=\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\";" + + " filename*=UTF-8\'\'File%25Name", contentDisposition.ToString()); + + contentDisposition.CreationDate = null; + Assert.Equal("inline; custom=\"custom value\"; filename*=UTF-8\'\'File%25Name", + contentDisposition.ToString()); + } + + [Fact] + public void GetHashCode_UseContentDispositionWithAndWithoutParameters_SameOrDifferentHashCodes() + { + var contentDisposition1 = new ContentDispositionHeaderValue("inline"); + var contentDisposition2 = new ContentDispositionHeaderValue("inline"); + contentDisposition2.Name = "myname"; + var contentDisposition3 = new ContentDispositionHeaderValue("inline"); + contentDisposition3.Parameters.Add(new NameValueHeaderValue("name", "value")); + var contentDisposition4 = new ContentDispositionHeaderValue("INLINE"); + var contentDisposition5 = new ContentDispositionHeaderValue("INLINE"); + contentDisposition5.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME")); + + Assert.NotEqual(contentDisposition1.GetHashCode(), contentDisposition2.GetHashCode()); + Assert.NotEqual(contentDisposition1.GetHashCode(), contentDisposition3.GetHashCode()); + Assert.NotEqual(contentDisposition2.GetHashCode(), contentDisposition3.GetHashCode()); + Assert.Equal(contentDisposition1.GetHashCode(), contentDisposition4.GetHashCode()); + Assert.Equal(contentDisposition2.GetHashCode(), contentDisposition5.GetHashCode()); + } + + [Fact] + public void Equals_UseContentDispositionWithAndWithoutParameters_EqualOrNotEqualNoExceptions() + { + var contentDisposition1 = new ContentDispositionHeaderValue("inline"); + var contentDisposition2 = new ContentDispositionHeaderValue("inline"); + contentDisposition2.Name = "myName"; + var contentDisposition3 = new ContentDispositionHeaderValue("inline"); + contentDisposition3.Parameters.Add(new NameValueHeaderValue("name", "value")); + var contentDisposition4 = new ContentDispositionHeaderValue("INLINE"); + var contentDisposition5 = new ContentDispositionHeaderValue("INLINE"); + contentDisposition5.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME")); + var contentDisposition6 = new ContentDispositionHeaderValue("INLINE"); + contentDisposition6.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME")); + contentDisposition6.Parameters.Add(new NameValueHeaderValue("custom", "value")); + var contentDisposition7 = new ContentDispositionHeaderValue("attachment"); + + Assert.False(contentDisposition1.Equals(contentDisposition2), "No params vs. name."); + Assert.False(contentDisposition2.Equals(contentDisposition1), "name vs. no params."); + Assert.False(contentDisposition1.Equals(null), "No params vs. <null>."); + Assert.False(contentDisposition1.Equals(contentDisposition3), "No params vs. custom param."); + Assert.False(contentDisposition2.Equals(contentDisposition3), "name vs. custom param."); + Assert.True(contentDisposition1.Equals(contentDisposition4), "Different casing."); + Assert.True(contentDisposition2.Equals(contentDisposition5), "Different casing in name."); + Assert.False(contentDisposition5.Equals(contentDisposition6), "name vs. custom param."); + Assert.False(contentDisposition1.Equals(contentDisposition7), "inline vs. text/other."); + } + + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + var expected = new ContentDispositionHeaderValue("inline"); + CheckValidParse("\r\n inline ", expected); + CheckValidParse("inline", expected); + + // We don't have to test all possible input strings, since most of the pieces are handled by other parsers. + // The purpose of this test is to verify that these other parsers are combined correctly to build a + // Content-Disposition parser. + expected.Name = "myName"; + CheckValidParse("\r\n inline ; name = myName ", expected); + CheckValidParse(" inline;name=myName", expected); + + expected.Name = null; + expected.DispositionType = "attachment"; + expected.FileName = "foo-ae.html"; + expected.Parameters.Add(new NameValueHeaderValue("filename*", "UTF-8''foo-%c3%a4.html")); + CheckValidParse(@"attachment; filename*=UTF-8''foo-%c3%a4.html; filename=foo-ae.html", expected); + } + + [Fact] + public void Parse_SetOfInvalidValueStrings_Throws() + { + CheckInvalidParse(""); + CheckInvalidParse(" "); + CheckInvalidParse(null); + CheckInvalidParse("inline会"); + CheckInvalidParse("inline ,"); + CheckInvalidParse("inline,"); + CheckInvalidParse("inline; name=myName ,"); + CheckInvalidParse("inline; name=myName,"); + CheckInvalidParse("inline; name=my会Name"); + CheckInvalidParse("inline/"); + } + + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + var expected = new ContentDispositionHeaderValue("inline"); + CheckValidTryParse("\r\n inline ", expected); + CheckValidTryParse("inline", expected); + + // We don't have to test all possible input strings, since most of the pieces are handled by other parsers. + // The purpose of this test is to verify that these other parsers are combined correctly to build a + // Content-Disposition parser. + expected.Name = "myName"; + CheckValidTryParse("\r\n inline ; name = myName ", expected); + CheckValidTryParse(" inline;name=myName", expected); + } + + [Fact] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() + { + CheckInvalidTryParse(""); + CheckInvalidTryParse(" "); + CheckInvalidTryParse(null); + CheckInvalidTryParse("inline会"); + CheckInvalidTryParse("inline ,"); + CheckInvalidTryParse("inline,"); + CheckInvalidTryParse("inline; name=myName ,"); + CheckInvalidTryParse("inline; name=myName,"); + CheckInvalidTryParse("text/"); + } + + public static TheoryData<string, ContentDispositionHeaderValue> ValidContentDispositionTestCases = new TheoryData<string, ContentDispositionHeaderValue>() + { + { "inline", new ContentDispositionHeaderValue("inline") }, // @"This should be equivalent to not including the header at all." + { "inline;", new ContentDispositionHeaderValue("inline") }, + { "inline;name=", new ContentDispositionHeaderValue("inline") { Parameters = { new NameValueHeaderValue("name", "") } } }, // TODO: passing in a null value causes a strange assert on CoreCLR before the test even starts. Not reproducable in the body of a test. + { "inline;name=value", new ContentDispositionHeaderValue("inline") { Name = "value" } }, + { "inline;name=value;", new ContentDispositionHeaderValue("inline") { Name = "value" } }, + { "inline;name=value;", new ContentDispositionHeaderValue("inline") { Name = "value" } }, + { @"inline; filename=""foo.html""", new ContentDispositionHeaderValue("inline") { FileName = @"""foo.html""" } }, + { @"inline; filename=""Not an attachment!""", new ContentDispositionHeaderValue("inline") { FileName = @"""Not an attachment!""" } }, // 'inline', specifying a filename of Not an attachment! - this checks for proper parsing for disposition types. + { @"inline; filename=""foo.pdf""", new ContentDispositionHeaderValue("inline") { FileName = @"""foo.pdf""" } }, + { "attachment", new ContentDispositionHeaderValue("attachment") }, + { "ATTACHMENT", new ContentDispositionHeaderValue("ATTACHMENT") }, + { @"attachment; filename=""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""" } }, + { @"attachment; filename=""\""quoting\"" tested.html""", new ContentDispositionHeaderValue("attachment") { FileName = "\"\"quoting\" tested.html\"" } }, // 'attachment', specifying a filename of \"quoting\" tested.html (using double quotes around "quoting" to test... quoting) + { @"attachment; filename=""Here's a semicolon;.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""Here's a semicolon;.html""" } }, // , 'attachment', specifying a filename of Here's a semicolon;.html - this checks for proper parsing for parameters. + { @"attachment; foo=""bar""; filename=""foo.html""", new ContentDispositionHeaderValue(@"attachment") { FileName = @"""foo.html""", Parameters = { new NameValueHeaderValue("foo", @"""bar""") } } }, // 'attachment', specifying a filename of foo.html and an extension parameter "foo" which should be ignored (see <a href="http://greenbytes.de/tech/webdav/rfc2183.html#rfc.section.2.8">Section 2.8 of RFC 2183</a>.). + { @"attachment; foo=""\""\\"";filename=""foo.html""", new ContentDispositionHeaderValue(@"attachment") { FileName = @"""foo.html""", Parameters = { new NameValueHeaderValue("foo", @"""\""\\""") } } }, // 'attachment', specifying a filename of foo.html and an extension parameter "foo" which should be ignored (see <a href="http://greenbytes.de/tech/webdav/rfc2183.html#rfc.section.2.8">Section 2.8 of RFC 2183</a>.). The extension parameter actually uses backslash-escapes. This tests whether the UA properly skips the parameter. + { @"attachment; FILENAME=""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""" } }, + { @"attachment; filename=foo.html", new ContentDispositionHeaderValue("attachment") { FileName = "foo.html" } }, // 'attachment', specifying a filename of foo.html using a token instead of a quoted-string. + { @"attachment; filename='foo.bar'", new ContentDispositionHeaderValue("attachment") { FileName = "'foo.bar'" } }, // 'attachment', specifying a filename of 'foo.bar' using single quotes. + { @"attachment; filename=""foo-ä.html""", new ContentDispositionHeaderValue("attachment" ) { Parameters = { new NameValueHeaderValue("filename", @"""foo-ä.html""") } } }, // 'attachment', specifying a filename of foo-ä.html, using plain ISO-8859-1 + { @"attachment; filename=""foo-ä.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-ä.html""" } }, // 'attachment', specifying a filename of foo-ä.html, which happens to be foo-ä.html using UTF-8 encoding. + { @"attachment; filename=""foo-%41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""foo-%41.html""") } } }, + { @"attachment; filename=""50%.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""50%.html""") } } }, + { @"attachment; filename=""foo-%\41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""foo-%\41.html""") } } }, // 'attachment', specifying a filename of foo-%41.html, using an escape character (this tests whether adding an escape character inside a %xx sequence can be used to disable the non-conformant %xx-unescaping). + { @"attachment; name=""foo-%41.html""", new ContentDispositionHeaderValue("attachment") { Name = @"""foo-%41.html""" } }, // 'attachment', specifying a <i>name</i> parameter of foo-%41.html. (this test was added to observe the behavior of the (unspecified) treatment of ""name"" as synonym for ""filename""; see <a href=""http://www.imc.org/ietf-smtp/mail-archive/msg05023.html"">Ned Freed's summary</a> where this comes from in MIME messages) + { @"attachment; filename=""ä-%41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""ä-%41.html""") } } }, // 'attachment', specifying a filename parameter of ä-%41.html. (this test was added to observe the behavior when non-ASCII characters and percent-hexdig sequences are combined) + { @"attachment; filename=""foo-%c3%a4-%e2%82%ac.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-%c3%a4-%e2%82%ac.html""" } }, // 'attachment', specifying a filename of foo-%c3%a4-%e2%82%ac.html, using raw percent encoded UTF-8 to represent foo-ä-€.html + { @"attachment; filename =""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""" } }, + { @"attachment; xfilename=foo.html", new ContentDispositionHeaderValue("attachment" ) { Parameters = { new NameValueHeaderValue("xfilename", "foo.html") } } }, + { @"attachment; filename=""/foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""/foo.html""" } }, + { @"attachment; creation-date=""Wed, 12 Feb 1997 16:29:51 -0500""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("creation-date", @"""Wed, 12 Feb 1997 16:29:51 -0500""") } } }, + { @"attachment; modification-date=""Wed, 12 Feb 1997 16:29:51 -0500""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("modification-date", @"""Wed, 12 Feb 1997 16:29:51 -0500""") } } }, + { @"foobar", new ContentDispositionHeaderValue("foobar") }, // @"This should be equivalent to using ""attachment""." + { @"attachment; example=""filename=example.txt""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("example", @"""filename=example.txt""") } } }, + { @"attachment; filename*=iso-8859-1''foo-%E4.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "iso-8859-1''foo-%E4.html") } } }, // 'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded ISO-8859-1 + { @"attachment; filename*=UTF-8''foo-%c3%a4-%e2%82%ac.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "UTF-8''foo-%c3%a4-%e2%82%ac.html") } } }, // 'attachment', specifying a filename of foo-ä-€.html, using RFC2231 encoded UTF-8 + { @"attachment; filename*=''foo-%c3%a4-%e2%82%ac.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "''foo-%c3%a4-%e2%82%ac.html") } } }, // Behavior is undefined in RFC 2231, the charset part is missing, although UTF-8 was used. + { @"attachment; filename*=UTF-8''foo-a%22.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = @"foo-a"".html" } }, + { @"attachment; filename*= UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html" } }, + { @"attachment; filename* =UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html" } }, + { @"attachment; filename*=UTF-8''A-%2541.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "A-%41.html" } }, + { @"attachment; filename*=UTF-8''%5cfoo.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = @"\foo.html" } }, + { @"attachment; filename=""foo-ae.html""; filename*=UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-ae.html""", FileNameStar = "foo-ä.html" } }, + { @"attachment; filename*=UTF-8''foo-%c3%a4.html; filename=""foo-ae.html""", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html", FileName = @"""foo-ae.html""" } }, + { @"attachment; foobar=x; filename=""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""", Parameters = { new NameValueHeaderValue("foobar", "x") } } }, + { @"attachment; filename=""=?ISO-8859-1?Q?foo-=E4.html?=""", new ContentDispositionHeaderValue("attachment") { FileName = @"""=?ISO-8859-1?Q?foo-=E4.html?=""" } }, // attachment; filename="=?ISO-8859-1?Q?foo-=E4.html?=" + { @"attachment; filename=""=?utf-8?B?Zm9vLeQuaHRtbA==?=""", new ContentDispositionHeaderValue("attachment") { FileName = @"""=?utf-8?B?Zm9vLeQuaHRtbA==?=""" } }, // attachment; filename="=?utf-8?B?Zm9vLeQuaHRtbA==?=" + { @"attachment; filename=foo.html ;", new ContentDispositionHeaderValue("attachment") { FileName="foo.html" } }, // 'attachment', specifying a filename of foo.html using a token instead of a quoted-string, and adding a trailing semicolon., + }; + + [Theory] + [MemberData(nameof(ValidContentDispositionTestCases))] + public void ContentDispositionHeaderValue_ParseValid_Success(string input, ContentDispositionHeaderValue expected) + { + // System.Diagnostics.Debugger.Launch(); + var result = ContentDispositionHeaderValue.Parse(input); + Assert.Equal(expected, result); + } + + [Theory] + // Invalid values + [InlineData(@"""inline""")] // @"'inline' only, using double quotes", false) }, + [InlineData(@"""attachment""")] // @"'attachment' only, using double quotes", false) }, + [InlineData(@"attachment; filename=foo bar.html")] // @"'attachment', specifying a filename of foo bar.html without using quoting.", false) }, + // Duplicate file name parameter + // @"attachment; filename=""foo.html""; // filename=""bar.html""", @"'attachment', specifying two filename parameters. This is invalid syntax.", false) }, + [InlineData(@"attachment; filename=foo[1](2).html")] // @"'attachment', specifying a filename of foo[1](2).html, but missing the quotes. Also, ""["", ""]"", ""("" and "")"" are not allowed in the HTTP <a href=""http://greenbytes.de/tech/webdav/draft-ietf-httpbis-p1-messaging-latest.html#rfc.section.1.2.2"">token</a> production.", false) }, + [InlineData(@"attachment; filename=foo-ä.html")] // @"'attachment', specifying a filename of foo-ä.html, but missing the quotes.", false) }, + // HTML escaping, not supported + // @"attachment; filename=foo-ä.html", // "'attachment', specifying a filename of foo-ä.html (which happens to be foo-ä.html using UTF-8 encoding) but missing the quotes.", false) }, + [InlineData(@"filename=foo.html")] // @"Disposition type missing, filename specified.", false) }, + [InlineData(@"x=y; filename=foo.html")] // @"Disposition type missing, filename specified after extension parameter.", false) }, + [InlineData(@"""foo; filename=bar;baz""; filename=qux")] // @"Disposition type missing, filename ""qux"". Can it be more broken? (Probably)", false) }, + [InlineData(@"filename=foo.html, filename=bar.html")] // @"Disposition type missing, two filenames specified separated by a comma (this is syntactically equivalent to have two instances of the header with one filename parameter each).", false) }, + [InlineData(@"; filename=foo.html")] // @"Disposition type missing (but delimiter present), filename specified.", false) }, + // This is permitted as a parameter without a value + // @"inline; attachment; filename=foo.html", // @"Both disposition types specified.", false) }, + // This is permitted as a parameter without a value + // @"inline; attachment; filename=foo.html", // @"Both disposition types specified.", false) }, + [InlineData(@"attachment; filename=""foo.html"".txt")] // @"'attachment', specifying a filename parameter that is broken (quoted-string followed by more characters). This is invalid syntax. ", false) }, + [InlineData(@"attachment; filename=""bar")] // @"'attachment', specifying a filename parameter that is broken (missing ending double quote). This is invalid syntax.", false) }, + [InlineData(@"attachment; filename=foo""bar;baz""qux")] // @"'attachment', specifying a filename parameter that is broken (disallowed characters in token syntax). This is invalid syntax.", false) }, + [InlineData(@"attachment; filename=foo.html, attachment; filename=bar.html")] // @"'attachment', two comma-separated instances of the header field. As Content-Disposition doesn't use a list-style syntax, this is invalid syntax and, according to <a href=""http://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.4.2.p.5"">RFC 2616, Section 4.2</a>, roughly equivalent to having two separate header field instances.", false) }, + [InlineData(@"filename=foo.html; attachment")] // @"filename parameter and disposition type reversed.", false) }, + // Escaping is not verified + // @"attachment; filename*=iso-8859-1''foo-%c3%a4-%e2%82%ac.html", // @"'attachment', specifying a filename of foo-ä-€.html, using RFC2231 encoded UTF-8, but declaring ISO-8859-1", false) }, + // Escaping is not verified + // @"attachment; filename *=UTF-8''foo-%c3%a4.html", // @"'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded UTF-8, with whitespace before ""*=""", false) }, + // Escaping is not verified + // @"attachment; filename*=""UTF-8''foo-%c3%a4.html""", // @"'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded UTF-8, with double quotes around the parameter value.", false) }, + [InlineData(@"attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=")] // @"Uses RFC 2047 style encoded word. ""="" is invalid inside the token production, so this is invalid.", false) }, + [InlineData(@"attachment; filename==?utf-8?B?Zm9vLeQuaHRtbA==?=")] // @"Uses RFC 2047 style encoded word. ""="" is invalid inside the token production, so this is invalid.", false) }, + public void ContentDispositionHeaderValue_ParseInvalid_Throws(string input) + { + Assert.Throws<FormatException>(() => ContentDispositionHeaderValue.Parse(input)); + } + + [Fact] + public void HeaderNamesWithQuotes_ExpectNamesToNotHaveQuotes() + { + var contentDispositionLine = "form-data; name =\"dotnet\"; filename=\"example.png\""; + var expectedName = "dotnet"; + var expectedFileName = "example.png"; + + var result = ContentDispositionHeaderValue.Parse(contentDispositionLine); + + Assert.Equal(expectedName, result.Name); + Assert.Equal(expectedFileName, result.FileName); + } + + public class ContentDispositionValue + { + public ContentDispositionValue(string value, string description, bool valid) + { + Value = value; + Description = description; + Valid = valid; + } + + public string Value { get; } + + public string Description { get; } + + public bool Valid { get; } + } + + private void CheckValidParse(string input, ContentDispositionHeaderValue expectedResult) + { + var result = ContentDispositionHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidParse(string input) + { + Assert.Throws<FormatException>(() => ContentDispositionHeaderValue.Parse(input)); + } + + private void CheckValidTryParse(string input, ContentDispositionHeaderValue expectedResult) + { + ContentDispositionHeaderValue result = null; + Assert.True(ContentDispositionHeaderValue.TryParse(input, out result), input); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidTryParse(string input) + { + ContentDispositionHeaderValue result = null; + Assert.False(ContentDispositionHeaderValue.TryParse(input, out result), input); + Assert.Null(result); + } + + private static void AssertFormatException(string contentDisposition) + { + Assert.Throws<FormatException>(() => new ContentDispositionHeaderValue(contentDisposition)); + } + } +} diff --git a/src/Http/Headers/test/ContentRangeHeaderValueTest.cs b/src/Http/Headers/test/ContentRangeHeaderValueTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..d8abdbdbf6b9e08098534b18f6e272aba7aadb3f --- /dev/null +++ b/src/Http/Headers/test/ContentRangeHeaderValueTest.cs @@ -0,0 +1,272 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class ContentRangeHeaderValueTest + { + [Fact] + public void Ctor_LengthOnlyOverloadUseInvalidValues_Throw() + { + Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(-1)); + } + + [Fact] + public void Ctor_LengthOnlyOverloadValidValues_ValuesCorrectlySet() + { + var range = new ContentRangeHeaderValue(5); + + Assert.False(range.HasRange, "HasRange"); + Assert.True(range.HasLength, "HasLength"); + Assert.Equal("bytes", range.Unit); + Assert.Null(range.From); + Assert.Null(range.To); + Assert.Equal(5, range.Length); + } + + [Fact] + public void Ctor_FromAndToOverloadUseInvalidValues_Throw() + { + Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(-1, 1)); + Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(0, -1)); + Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(2, 1)); + } + + [Fact] + public void Ctor_FromAndToOverloadValidValues_ValuesCorrectlySet() + { + var range = new ContentRangeHeaderValue(0, 1); + + Assert.True(range.HasRange, "HasRange"); + Assert.False(range.HasLength, "HasLength"); + Assert.Equal("bytes", range.Unit); + Assert.Equal(0, range.From); + Assert.Equal(1, range.To); + Assert.Null(range.Length); + } + + [Fact] + public void Ctor_FromToAndLengthOverloadUseInvalidValues_Throw() + { + Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(-1, 1, 2)); + Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(0, -1, 2)); + Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(0, 1, -1)); + Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(2, 1, 3)); + Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(1, 2, 1)); + } + + [Fact] + public void Ctor_FromToAndLengthOverloadValidValues_ValuesCorrectlySet() + { + var range = new ContentRangeHeaderValue(0, 1, 2); + + Assert.True(range.HasRange, "HasRange"); + Assert.True(range.HasLength, "HasLength"); + Assert.Equal("bytes", range.Unit); + Assert.Equal(0, range.From); + Assert.Equal(1, range.To); + Assert.Equal(2, range.Length); + } + + [Fact] + public void Unit_GetAndSetValidAndInvalidValues_MatchExpectation() + { + var range = new ContentRangeHeaderValue(0); + range.Unit = "myunit"; + Assert.Equal("myunit", range.Unit); + + Assert.Throws<ArgumentException>(() => range.Unit = null); + Assert.Throws<ArgumentException>(() => range.Unit = ""); + Assert.Throws<FormatException>(() => range.Unit = " x"); + Assert.Throws<FormatException>(() => range.Unit = "x "); + Assert.Throws<FormatException>(() => range.Unit = "x y"); + } + + [Fact] + public void ToString_UseDifferentRanges_AllSerializedCorrectly() + { + var range = new ContentRangeHeaderValue(1, 2, 3); + range.Unit = "myunit"; + Assert.Equal("myunit 1-2/3", range.ToString()); + + range = new ContentRangeHeaderValue(123456789012345678, 123456789012345679); + Assert.Equal("bytes 123456789012345678-123456789012345679/*", range.ToString()); + + range = new ContentRangeHeaderValue(150); + Assert.Equal("bytes */150", range.ToString()); + } + + [Fact] + public void GetHashCode_UseSameAndDifferentRanges_SameOrDifferentHashCodes() + { + var range1 = new ContentRangeHeaderValue(1, 2, 5); + var range2 = new ContentRangeHeaderValue(1, 2); + var range3 = new ContentRangeHeaderValue(5); + var range4 = new ContentRangeHeaderValue(1, 2, 5); + range4.Unit = "BYTES"; + var range5 = new ContentRangeHeaderValue(1, 2, 5); + range5.Unit = "myunit"; + + Assert.NotEqual(range1.GetHashCode(), range2.GetHashCode()); + Assert.NotEqual(range1.GetHashCode(), range3.GetHashCode()); + Assert.NotEqual(range2.GetHashCode(), range3.GetHashCode()); + Assert.Equal(range1.GetHashCode(), range4.GetHashCode()); + Assert.NotEqual(range1.GetHashCode(), range5.GetHashCode()); + } + + [Fact] + public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() + { + var range1 = new ContentRangeHeaderValue(1, 2, 5); + var range2 = new ContentRangeHeaderValue(1, 2); + var range3 = new ContentRangeHeaderValue(5); + var range4 = new ContentRangeHeaderValue(1, 2, 5); + range4.Unit = "BYTES"; + var range5 = new ContentRangeHeaderValue(1, 2, 5); + range5.Unit = "myunit"; + var range6 = new ContentRangeHeaderValue(1, 3, 5); + var range7 = new ContentRangeHeaderValue(2, 2, 5); + var range8 = new ContentRangeHeaderValue(1, 2, 6); + + Assert.False(range1.Equals(null), "bytes 1-2/5 vs. <null>"); + Assert.False(range1.Equals(range2), "bytes 1-2/5 vs. bytes 1-2/*"); + Assert.False(range1.Equals(range3), "bytes 1-2/5 vs. bytes */5"); + Assert.False(range2.Equals(range3), "bytes 1-2/* vs. bytes */5"); + Assert.True(range1.Equals(range4), "bytes 1-2/5 vs. BYTES 1-2/5"); + Assert.True(range4.Equals(range1), "BYTES 1-2/5 vs. bytes 1-2/5"); + Assert.False(range1.Equals(range5), "bytes 1-2/5 vs. myunit 1-2/5"); + Assert.False(range1.Equals(range6), "bytes 1-2/5 vs. bytes 1-3/5"); + Assert.False(range1.Equals(range7), "bytes 1-2/5 vs. bytes 2-2/5"); + Assert.False(range1.Equals(range8), "bytes 1-2/5 vs. bytes 1-2/6"); + } + + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidParse(" bytes 1-2/3 ", new ContentRangeHeaderValue(1, 2, 3)); + CheckValidParse("bytes * / 3", new ContentRangeHeaderValue(3)); + + CheckValidParse(" custom 1234567890123456789-1234567890123456799/*", + new ContentRangeHeaderValue(1234567890123456789, 1234567890123456799) { Unit = "custom" }); + + CheckValidParse(" custom * / 123 ", + new ContentRangeHeaderValue(123) { Unit = "custom" }); + + // Note that we don't have a public constructor for value 'bytes */*' since the RFC doesn't mention a + // scenario for it. However, if a server returns this value, we're flexible and accept it. + var result = ContentRangeHeaderValue.Parse("bytes */*"); + Assert.Equal("bytes", result.Unit); + Assert.Null(result.From); + Assert.Null(result.To); + Assert.Null(result.Length); + Assert.False(result.HasRange, "HasRange"); + Assert.False(result.HasLength, "HasLength"); + } + + [Theory] + [InlineData("bytes 1-2/3,")] // no character after 'length' allowed + [InlineData("x bytes 1-2/3")] + [InlineData("bytes 1-2/3.4")] + [InlineData(null)] + [InlineData("")] + [InlineData("bytes 3-2/5")] + [InlineData("bytes 6-6/5")] + [InlineData("bytes 1-6/5")] + [InlineData("bytes 1-2/")] + [InlineData("bytes 1-2")] + [InlineData("bytes 1-/")] + [InlineData("bytes 1-")] + [InlineData("bytes 1")] + [InlineData("bytes ")] + [InlineData("bytes a-2/3")] + [InlineData("bytes 1-b/3")] + [InlineData("bytes 1-2/c")] + [InlineData("bytes1-2/3")] + // More than 19 digits >>Int64.MaxValue + [InlineData("bytes 1-12345678901234567890/3")] + [InlineData("bytes 12345678901234567890-3/3")] + [InlineData("bytes 1-2/12345678901234567890")] + // Exceed Int64.MaxValue, but use 19 digits + [InlineData("bytes 1-9999999999999999999/3")] + [InlineData("bytes 9999999999999999999-3/3")] + [InlineData("bytes 1-2/9999999999999999999")] + public void Parse_SetOfInvalidValueStrings_Throws(string input) + { + Assert.Throws<FormatException>(() => ContentRangeHeaderValue.Parse(input)); + } + + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidTryParse(" bytes 1-2/3 ", new ContentRangeHeaderValue(1, 2, 3)); + CheckValidTryParse("bytes * / 3", new ContentRangeHeaderValue(3)); + + CheckValidTryParse(" custom 1234567890123456789-1234567890123456799/*", + new ContentRangeHeaderValue(1234567890123456789, 1234567890123456799) { Unit = "custom" }); + + CheckValidTryParse(" custom * / 123 ", + new ContentRangeHeaderValue(123) { Unit = "custom" }); + + // Note that we don't have a public constructor for value 'bytes */*' since the RFC doesn't mention a + // scenario for it. However, if a server returns this value, we're flexible and accept it. + ContentRangeHeaderValue result = null; + Assert.True(ContentRangeHeaderValue.TryParse("bytes */*", out result)); + Assert.Equal("bytes", result.Unit); + Assert.Null(result.From); + Assert.Null(result.To); + Assert.Null(result.Length); + Assert.False(result.HasRange, "HasRange"); + Assert.False(result.HasLength, "HasLength"); + } + + [Theory] + [InlineData("bytes 1-2/3,")] // no character after 'length' allowed + [InlineData("x bytes 1-2/3")] + [InlineData("bytes 1-2/3.4")] + [InlineData(null)] + [InlineData("")] + [InlineData("bytes 3-2/5")] + [InlineData("bytes 6-6/5")] + [InlineData("bytes 1-6/5")] + [InlineData("bytes 1-2/")] + [InlineData("bytes 1-2")] + [InlineData("bytes 1-/")] + [InlineData("bytes 1-")] + [InlineData("bytes 1")] + [InlineData("bytes ")] + [InlineData("bytes a-2/3")] + [InlineData("bytes 1-b/3")] + [InlineData("bytes 1-2/c")] + [InlineData("bytes1-2/3")] + // More than 19 digits >>Int64.MaxValue + [InlineData("bytes 1-12345678901234567890/3")] + [InlineData("bytes 12345678901234567890-3/3")] + [InlineData("bytes 1-2/12345678901234567890")] + // Exceed Int64.MaxValue, but use 19 digits + [InlineData("bytes 1-9999999999999999999/3")] + [InlineData("bytes 9999999999999999999-3/3")] + [InlineData("bytes 1-2/9999999999999999999")] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse(string input) + { + ContentRangeHeaderValue result = null; + Assert.False(ContentRangeHeaderValue.TryParse(input, out result)); + Assert.Null(result); + } + + private void CheckValidParse(string input, ContentRangeHeaderValue expectedResult) + { + var result = ContentRangeHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } + + private void CheckValidTryParse(string input, ContentRangeHeaderValue expectedResult) + { + ContentRangeHeaderValue result = null; + Assert.True(ContentRangeHeaderValue.TryParse(input, out result)); + Assert.Equal(expectedResult, result); + } + } +} diff --git a/src/Http/Headers/test/CookieHeaderValueTest.cs b/src/Http/Headers/test/CookieHeaderValueTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..416441991d7cc15abfdb6535d8e01b75ca18f676 --- /dev/null +++ b/src/Http/Headers/test/CookieHeaderValueTest.cs @@ -0,0 +1,326 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class CookieHeaderValueTest + { + public static TheoryData<CookieHeaderValue, string> CookieHeaderDataSet + { + get + { + var dataset = new TheoryData<CookieHeaderValue, string>(); + var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3"); + dataset.Add(header1, "name1=n1=v1&n2=v2&n3=v3"); + + var header2 = new CookieHeaderValue("name2", ""); + dataset.Add(header2, "name2="); + + var header3 = new CookieHeaderValue("name3", "value3"); + dataset.Add(header3, "name3=value3"); + + var header4 = new CookieHeaderValue("name4", "\"value4\""); + dataset.Add(header4, "name4=\"value4\""); + + return dataset; + } + } + + public static TheoryData<string> InvalidCookieHeaderDataSet + { + get + { + return new TheoryData<string> + { + "=value", + "name=value;", + "name=value,", + }; + } + } + + public static TheoryData<string> InvalidCookieNames + { + get + { + return new TheoryData<string> + { + "<acb>", + "{acb}", + "[acb]", + "\"acb\"", + "a,b", + "a;b", + "a\\b", + "a b", + }; + } + } + + public static TheoryData<string> InvalidCookieValues + { + get + { + return new TheoryData<string> + { + { "\"" }, + { "a,b" }, + { "a;b" }, + { "a\\b" }, + { "\"abc" }, + { "a\"bc" }, + { "abc\"" }, + { "a b" }, + }; + } + } + + public static TheoryData<IList<CookieHeaderValue>, string[]> ListOfCookieHeaderDataSet + { + get + { + var dataset = new TheoryData<IList<CookieHeaderValue>, string[]>(); + var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3"); + var string1 = "name1=n1=v1&n2=v2&n3=v3"; + + var header2 = new CookieHeaderValue("name2", "value2"); + var string2 = "name2=value2"; + + var header3 = new CookieHeaderValue("name3", "value3"); + var string3 = "name3=value3"; + + var header4 = new CookieHeaderValue("name4", "\"value4\""); + var string4 = "name4=\"value4\""; + + dataset.Add(new[] { header1 }.ToList(), new[] { string1 }); + dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 }); + dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ";", " , ", string1 }); + dataset.Add(new[] { header2 }.ToList(), new[] { string2 }); + dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1, string2 }); + dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1 + ", " + string2 }); + dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + "; " + string1 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(";", string1, string2, string3, string4) }); + + return dataset; + } + } + + public static TheoryData<IList<CookieHeaderValue>, string[]> ListWithInvalidCookieHeaderDataSet + { + get + { + var dataset = new TheoryData<IList<CookieHeaderValue>, string[]>(); + var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3"); + var validString1 = "name1=n1=v1&n2=v2&n3=v3"; + + var header2 = new CookieHeaderValue("name2", "value2"); + var validString2 = "name2=value2"; + + var header3 = new CookieHeaderValue("name3", "value3"); + var validString3 = "name3=value3"; + + var invalidString1 = "ipt={\"v\":{\"L\":3},\"pt\":{\"d\":3},ct\":{},\"_t\":44,\"_v\":\"2\"}"; + + dataset.Add(null, new[] { invalidString1 }); + dataset.Add(new[] { header1 }.ToList(), new[] { validString1, invalidString1 }); + dataset.Add(new[] { header1 }.ToList(), new[] { validString1, null, "", " ", ";", " , ", invalidString1 }); + dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1, null, "", " ", ";", " , ", validString1 }); + dataset.Add(new[] { header1 }.ToList(), new[] { validString1 + ", " + invalidString1 }); + dataset.Add(new[] { header2 }.ToList(), new[] { invalidString1 + ", " + validString2 }); + dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1 + "; " + validString1 }); + dataset.Add(new[] { header2 }.ToList(), new[] { validString2 + "; " + invalidString1 }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { invalidString1, validString1, validString2, validString3 }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, invalidString1, validString2, validString3 }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, validString2, invalidString1, validString3 }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, validString2, validString3, invalidString1 }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", invalidString1, validString1, validString2, validString3) }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, invalidString1, validString2, validString3) }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, validString2, invalidString1, validString3) }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, validString2, validString3, invalidString1) }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", invalidString1, validString1, validString2, validString3) }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, invalidString1, validString2, validString3) }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, validString2, invalidString1, validString3) }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, validString2, validString3, invalidString1) }); + + return dataset; + } + } + + [Fact] + public void CookieHeaderValue_CtorThrowsOnNullName() + { + Assert.Throws<ArgumentNullException>(() => new CookieHeaderValue(null, "value")); + } + + [Theory] + [MemberData(nameof(InvalidCookieNames))] + public void CookieHeaderValue_CtorThrowsOnInvalidName(string name) + { + Assert.Throws<ArgumentException>(() => new CookieHeaderValue(name, "value")); + } + + [Theory] + [MemberData(nameof(InvalidCookieValues))] + public void CookieHeaderValue_CtorThrowsOnInvalidValue(string value) + { + Assert.Throws<ArgumentException>(() => new CookieHeaderValue("name", value)); + } + + [Fact] + public void CookieHeaderValue_Ctor1_InitializesCorrectly() + { + var header = new CookieHeaderValue("cookie"); + Assert.Equal("cookie", header.Name); + Assert.Equal(string.Empty, header.Value); + } + + [Theory] + [InlineData("name", "")] + [InlineData("name", "value")] + [InlineData("name", "\"acb\"")] + public void CookieHeaderValue_Ctor2InitializesCorrectly(string name, string value) + { + var header = new CookieHeaderValue(name, value); + Assert.Equal(name, header.Name); + Assert.Equal(value, header.Value); + } + + [Fact] + public void CookieHeaderValue_Value() + { + var cookie = new CookieHeaderValue("name"); + Assert.Equal(string.Empty, cookie.Value); + + cookie.Value = "value1"; + Assert.Equal("value1", cookie.Value); + } + + [Theory] + [MemberData(nameof(CookieHeaderDataSet))] + public void CookieHeaderValue_ToString(CookieHeaderValue input, string expectedValue) + { + Assert.Equal(expectedValue, input.ToString()); + } + + [Theory] + [MemberData(nameof(CookieHeaderDataSet))] + public void CookieHeaderValue_Parse_AcceptsValidValues(CookieHeaderValue cookie, string expectedValue) + { + var header = CookieHeaderValue.Parse(expectedValue); + + Assert.Equal(cookie, header); + Assert.Equal(expectedValue, header.ToString()); + } + + [Theory] + [MemberData(nameof(CookieHeaderDataSet))] + public void CookieHeaderValue_TryParse_AcceptsValidValues(CookieHeaderValue cookie, string expectedValue) + { + Assert.True(CookieHeaderValue.TryParse(expectedValue, out var header)); + + Assert.Equal(cookie, header); + Assert.Equal(expectedValue, header.ToString()); + } + + [Theory] + [MemberData(nameof(InvalidCookieHeaderDataSet))] + public void CookieHeaderValue_Parse_RejectsInvalidValues(string value) + { + Assert.Throws<FormatException>(() => CookieHeaderValue.Parse(value)); + } + + [Theory] + [MemberData(nameof(InvalidCookieHeaderDataSet))] + public void CookieHeaderValue_TryParse_RejectsInvalidValues(string value) + { + Assert.False(CookieHeaderValue.TryParse(value, out var _)); + } + + [Theory] + [MemberData(nameof(ListOfCookieHeaderDataSet))] + public void CookieHeaderValue_ParseList_AcceptsValidValues(IList<CookieHeaderValue> cookies, string[] input) + { + var results = CookieHeaderValue.ParseList(input); + + Assert.Equal(cookies, results); + } + + [Theory] + [MemberData(nameof(ListOfCookieHeaderDataSet))] + public void CookieHeaderValue_ParseStrictList_AcceptsValidValues(IList<CookieHeaderValue> cookies, string[] input) + { + var results = CookieHeaderValue.ParseStrictList(input); + + Assert.Equal(cookies, results); + } + + [Theory] + [MemberData(nameof(ListOfCookieHeaderDataSet))] + public void CookieHeaderValue_TryParseList_AcceptsValidValues(IList<CookieHeaderValue> cookies, string[] input) + { + var result = CookieHeaderValue.TryParseList(input, out var results); + Assert.True(result); + + Assert.Equal(cookies, results); + } + + [Theory] + [MemberData(nameof(ListOfCookieHeaderDataSet))] + public void CookieHeaderValue_TryParseStrictList_AcceptsValidValues(IList<CookieHeaderValue> cookies, string[] input) + { + var result = CookieHeaderValue.TryParseStrictList(input, out var results); + Assert.True(result); + + Assert.Equal(cookies, results); + } + + [Theory] + [MemberData(nameof(ListWithInvalidCookieHeaderDataSet))] + public void CookieHeaderValue_ParseList_ExcludesInvalidValues(IList<CookieHeaderValue> cookies, string[] input) + { + var results = CookieHeaderValue.ParseList(input); + // ParseList aways returns a list, even if empty. TryParseList may return null (via out). + Assert.Equal(cookies ?? new List<CookieHeaderValue>(), results); + } + + [Theory] + [MemberData(nameof(ListWithInvalidCookieHeaderDataSet))] + public void CookieHeaderValue_TryParseList_ExcludesInvalidValues(IList<CookieHeaderValue> cookies, string[] input) + { + var result = CookieHeaderValue.TryParseList(input, out var results); + Assert.Equal(cookies, results); + Assert.Equal(cookies?.Count > 0, result); + } + + [Theory] + [MemberData(nameof(ListWithInvalidCookieHeaderDataSet))] + public void CookieHeaderValue_ParseStrictList_ThrowsForAnyInvalidValues( +#pragma warning disable xUnit1026 // Theory methods should use all of their parameters + IList<CookieHeaderValue> cookies, +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + string[] input) + { + Assert.Throws<FormatException>(() => CookieHeaderValue.ParseStrictList(input)); + } + + [Theory] + [MemberData(nameof(ListWithInvalidCookieHeaderDataSet))] + public void CookieHeaderValue_TryParseStrictList_FailsForAnyInvalidValues( +#pragma warning disable xUnit1026 // Theory methods should use all of their parameters + IList<CookieHeaderValue> cookies, +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + string[] input) + { + var result = CookieHeaderValue.TryParseStrictList(input, out var results); + Assert.Null(results); + Assert.False(result); + } + } +} diff --git a/src/Http/Headers/test/DateParserTest.cs b/src/Http/Headers/test/DateParserTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..5c211c4368446376cf9382eadafebaec997d9cbf --- /dev/null +++ b/src/Http/Headers/test/DateParserTest.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class DateParserTest + { + [Theory] + [MemberData(nameof(ValidStringData))] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly(string input, DateTimeOffset expected) + { + // We don't need to validate all possible date values, since they're already tested in HttpRuleParserTest. + // Just make sure the parser calls HttpRuleParser methods correctly. + Assert.True(HeaderUtilities.TryParseDate(input, out var result)); + Assert.Equal(expected, result); + } + + public static IEnumerable<object[]> ValidStringData() + { + yield return new object[] { "Tue, 15 Nov 1994 08:12:31 GMT", new DateTimeOffset(1994, 11, 15, 8, 12, 31, TimeSpan.Zero) }; + yield return new object[] { " Sunday, 06-Nov-94 08:49:37 GMT ", new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero) }; + yield return new object[] { " Tue,\r\n 15 Nov\r\n 1994 08:12:31 GMT ", new DateTimeOffset(1994, 11, 15, 8, 12, 31, TimeSpan.Zero) }; + yield return new object[] { "Sat, 09-Dec-2017 07:07:03 GMT ", new DateTimeOffset(2017, 12, 09, 7, 7, 3, TimeSpan.Zero) }; + } + + [Theory] + [MemberData(nameof(InvalidStringData))] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse(string input) + { + Assert.False(HeaderUtilities.TryParseDate(input, out var result)); + Assert.Equal(new DateTimeOffset(), result); + } + + public static IEnumerable<object[]> InvalidStringData() + { + yield return new object[] { null }; + yield return new object[] { string.Empty }; + yield return new object[] { " " }; + yield return new object[] { "!!Sunday, 06-Nov-94 08:49:37 GMT" }; + } + + [Fact] + public void ToString_UseDifferentValues_MatchExpectation() + { + Assert.Equal("Sat, 31 Jul 2010 15:38:57 GMT", + HeaderUtilities.FormatDate(new DateTimeOffset(2010, 7, 31, 15, 38, 57, TimeSpan.Zero))); + + Assert.Equal("Fri, 01 Jan 2010 01:01:01 GMT", + HeaderUtilities.FormatDate(new DateTimeOffset(2010, 1, 1, 1, 1, 1, TimeSpan.Zero))); + } + } +} diff --git a/src/Http/Headers/test/EntityTagHeaderValueTest.cs b/src/Http/Headers/test/EntityTagHeaderValueTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..f633fec2264f1212b67939d1d918ef57c402feef --- /dev/null +++ b/src/Http/Headers/test/EntityTagHeaderValueTest.cs @@ -0,0 +1,533 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class EntityTagHeaderValueTest + { + [Fact] + public void Ctor_ETagNull_Throw() + { + Assert.Throws<ArgumentException>(() => new EntityTagHeaderValue(null)); + // null and empty should be treated the same. So we also throw for empty strings. + Assert.Throws<ArgumentException>(() => new EntityTagHeaderValue(string.Empty)); + } + + [Fact] + public void Ctor_ETagInvalidFormat_ThrowFormatException() + { + // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. + AssertFormatException("tag"); + AssertFormatException(" tag "); + AssertFormatException("\"tag\" invalid"); + AssertFormatException("\"tag"); + AssertFormatException("tag\""); + AssertFormatException("\"tag\"\""); + AssertFormatException("\"\"tag\"\""); + AssertFormatException("\"\"tag\""); + AssertFormatException("W/\"tag\""); // tag value must not contain 'W/' + } + + [Fact] + public void Ctor_ETagValidFormat_SuccessfullyCreated() + { + var etag = new EntityTagHeaderValue("\"tag\""); + Assert.Equal("\"tag\"", etag.Tag); + Assert.False(etag.IsWeak, "IsWeak"); + } + + [Fact] + public void Ctor_ETagValidFormatAndIsWeak_SuccessfullyCreated() + { + var etag = new EntityTagHeaderValue("\"e tag\"", true); + Assert.Equal("\"e tag\"", etag.Tag); + Assert.True(etag.IsWeak, "IsWeak"); + } + + [Fact] + public void ToString_UseDifferentETags_AllSerializedCorrectly() + { + var etag = new EntityTagHeaderValue("\"e tag\""); + Assert.Equal("\"e tag\"", etag.ToString()); + + etag = new EntityTagHeaderValue("\"e tag\"", true); + Assert.Equal("W/\"e tag\"", etag.ToString()); + + etag = new EntityTagHeaderValue("\"\"", false); + Assert.Equal("\"\"", etag.ToString()); + } + + [Fact] + public void GetHashCode_UseSameAndDifferentETags_SameOrDifferentHashCodes() + { + var etag1 = new EntityTagHeaderValue("\"tag\""); + var etag2 = new EntityTagHeaderValue("\"TAG\""); + var etag3 = new EntityTagHeaderValue("\"tag\"", true); + var etag4 = new EntityTagHeaderValue("\"tag1\""); + var etag5 = new EntityTagHeaderValue("\"tag\""); + var etag6 = EntityTagHeaderValue.Any; + + Assert.NotEqual(etag1.GetHashCode(), etag2.GetHashCode()); + Assert.NotEqual(etag1.GetHashCode(), etag3.GetHashCode()); + Assert.NotEqual(etag1.GetHashCode(), etag4.GetHashCode()); + Assert.NotEqual(etag1.GetHashCode(), etag6.GetHashCode()); + Assert.Equal(etag1.GetHashCode(), etag5.GetHashCode()); + } + + [Fact] + public void Equals_UseSameAndDifferentETags_EqualOrNotEqualNoExceptions() + { + var etag1 = new EntityTagHeaderValue("\"tag\""); + var etag2 = new EntityTagHeaderValue("\"TAG\""); + var etag3 = new EntityTagHeaderValue("\"tag\"", true); + var etag4 = new EntityTagHeaderValue("\"tag1\""); + var etag5 = new EntityTagHeaderValue("\"tag\""); + var etag6 = EntityTagHeaderValue.Any; + + Assert.False(etag1.Equals(etag2), "Different casing."); + Assert.False(etag2.Equals(etag1), "Different casing."); + Assert.False(etag1.Equals(null), "tag vs. <null>."); + Assert.False(etag1.Equals(etag3), "strong vs. weak."); + Assert.False(etag3.Equals(etag1), "weak vs. strong."); + Assert.False(etag1.Equals(etag4), "tag vs. tag1."); + Assert.False(etag1.Equals(etag6), "tag vs. *."); + Assert.True(etag1.Equals(etag5), "tag vs. tag.."); + } + + [Fact] + public void Compare_WithNull_ReturnsFalse() + { + Assert.False(EntityTagHeaderValue.Any.Compare(null, useStrongComparison: true)); + Assert.False(EntityTagHeaderValue.Any.Compare(null, useStrongComparison: false)); + } + + public static TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> NotEquivalentUnderStrongComparison + { + get + { + return new TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> + { + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"TAG\"") }, + { new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"", true) }, + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"", true) }, + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag1\"") }, + { new EntityTagHeaderValue("\"tag\""), EntityTagHeaderValue.Any }, + }; + } + } + + [Theory] + [MemberData(nameof(NotEquivalentUnderStrongComparison))] + public void CompareUsingStrongComparison_NonEquivalentPairs_ReturnFalse(EntityTagHeaderValue left, EntityTagHeaderValue right) + { + Assert.False(left.Compare(right, useStrongComparison: true)); + Assert.False(right.Compare(left, useStrongComparison: true)); + } + + public static TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> EquivalentUnderStrongComparison + { + get + { + return new TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> + { + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"") }, + }; + } + } + + [Theory] + [MemberData(nameof(EquivalentUnderStrongComparison))] + public void CompareUsingStrongComparison_EquivalentPairs_ReturnTrue(EntityTagHeaderValue left, EntityTagHeaderValue right) + { + Assert.True(left.Compare(right, useStrongComparison: true)); + Assert.True(right.Compare(left, useStrongComparison: true)); + } + + public static TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> NotEquivalentUnderWeakComparison + { + get + { + return new TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> + { + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"TAG\"") }, + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag1\"") }, + { new EntityTagHeaderValue("\"tag\""), EntityTagHeaderValue.Any }, + }; + } + } + + [Theory] + [MemberData(nameof(NotEquivalentUnderWeakComparison))] + public void CompareUsingWeakComparison_NonEquivalentPairs_ReturnFalse(EntityTagHeaderValue left, EntityTagHeaderValue right) + { + Assert.False(left.Compare(right, useStrongComparison: false)); + Assert.False(right.Compare(left, useStrongComparison: false)); + } + + public static TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> EquivalentUnderWeakComparison + { + get + { + return new TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> + { + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"") }, + { new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"", true) }, + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"", true) }, + }; + } + } + + [Theory] + [MemberData(nameof(EquivalentUnderWeakComparison))] + public void CompareUsingWeakComparison_EquivalentPairs_ReturnTrue(EntityTagHeaderValue left, EntityTagHeaderValue right) + { + Assert.True(left.Compare(right, useStrongComparison: false)); + Assert.True(right.Compare(left, useStrongComparison: false)); + } + + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidParse("\"tag\"", new EntityTagHeaderValue("\"tag\"")); + CheckValidParse(" \"tag\" ", new EntityTagHeaderValue("\"tag\"")); + CheckValidParse("\r\n \"tag\"\r\n ", new EntityTagHeaderValue("\"tag\"")); + CheckValidParse("\"tag\"", new EntityTagHeaderValue("\"tag\"")); + CheckValidParse("\"tag会\"", new EntityTagHeaderValue("\"tag会\"")); + CheckValidParse("W/\"tag\"", new EntityTagHeaderValue("\"tag\"", true)); + CheckValidParse("*", new EntityTagHeaderValue("*")); + } + + [Fact] + public void Parse_SetOfInvalidValueStrings_Throws() + { + CheckInvalidParse(null); + CheckInvalidParse(string.Empty); + CheckInvalidParse(" "); + CheckInvalidParse(" !"); + CheckInvalidParse("tag\" !"); + CheckInvalidParse("!\"tag\""); + CheckInvalidParse("\"tag\","); + CheckInvalidParse("W"); + CheckInvalidParse("W/"); + CheckInvalidParse("W/\""); + CheckInvalidParse("\"tag\" \"tag2\""); + CheckInvalidParse("/\"tag\""); + } + + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidTryParse("\"tag\"", new EntityTagHeaderValue("\"tag\"")); + CheckValidTryParse(" \"tag\" ", new EntityTagHeaderValue("\"tag\"")); + CheckValidTryParse("\r\n \"tag\"\r\n ", new EntityTagHeaderValue("\"tag\"")); + CheckValidTryParse("\"tag\"", new EntityTagHeaderValue("\"tag\"")); + CheckValidTryParse("\"tag会\"", new EntityTagHeaderValue("\"tag会\"")); + CheckValidTryParse("W/\"tag\"", new EntityTagHeaderValue("\"tag\"", true)); + CheckValidTryParse("*", new EntityTagHeaderValue("*")); + } + + [Fact] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() + { + CheckInvalidTryParse(null); + CheckInvalidTryParse(string.Empty); + CheckInvalidTryParse(" "); + CheckInvalidTryParse(" !"); + CheckInvalidTryParse("tag\" !"); + CheckInvalidTryParse("!\"tag\""); + CheckInvalidTryParse("\"tag\","); + CheckInvalidTryParse("\"tag\" \"tag2\""); + CheckInvalidTryParse("/\"tag\""); + } + + [Fact] + public void ParseList_NullOrEmptyArray_ReturnsEmptyList() + { + var result = EntityTagHeaderValue.ParseList(null); + Assert.NotNull(result); + Assert.Equal(0, result.Count); + + result = EntityTagHeaderValue.ParseList(new string[0]); + Assert.NotNull(result); + Assert.Equal(0, result.Count); + + result = EntityTagHeaderValue.ParseList(new string[] { "" }); + Assert.NotNull(result); + Assert.Equal(0, result.Count); + } + + [Fact] + public void TryParseList_NullOrEmptyArray_ReturnsFalse() + { + IList<EntityTagHeaderValue> results = null; + Assert.False(EntityTagHeaderValue.TryParseList(null, out results)); + Assert.False(EntityTagHeaderValue.TryParseList(new string[0], out results)); + Assert.False(EntityTagHeaderValue.TryParseList(new string[] { "" }, out results)); + } + + [Fact] + public void ParseList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] + { + "", + "\"tag\"", + "", + " \"tag\" ", + "\r\n \"tag\"\r\n ", + "\"tag会\"", + "\"tag\",\"tag\"", + "\"tag\", \"tag\"", + "W/\"tag\"", + }; + IList<EntityTagHeaderValue> results = EntityTagHeaderValue.ParseList(inputs); + + var expectedResults = new[] + { + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag会\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\"", true), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void ParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] + { + "", + "\"tag\"", + "", + " \"tag\" ", + "\r\n \"tag\"\r\n ", + "\"tag会\"", + "\"tag\",\"tag\"", + "\"tag\", \"tag\"", + "W/\"tag\"", + }; + IList<EntityTagHeaderValue> results = EntityTagHeaderValue.ParseStrictList(inputs); + + var expectedResults = new[] + { + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag会\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\"", true), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void TryParseList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] + { + "", + "\"tag\"", + "", + " \"tag\" ", + "\r\n \"tag\"\r\n ", + "\"tag会\"", + "\"tag\",\"tag\"", + "\"tag\", \"tag\"", + "W/\"tag\"", + }; + IList<EntityTagHeaderValue> results; + Assert.True(EntityTagHeaderValue.TryParseList(inputs, out results)); + var expectedResults = new[] + { + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag会\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\"", true), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void TryParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] + { + "", + "\"tag\"", + "", + " \"tag\" ", + "\r\n \"tag\"\r\n ", + "\"tag会\"", + "\"tag\",\"tag\"", + "\"tag\", \"tag\"", + "W/\"tag\"", + }; + IList<EntityTagHeaderValue> results; + Assert.True(EntityTagHeaderValue.TryParseStrictList(inputs, out results)); + var expectedResults = new[] + { + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag会\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\"", true), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void ParseList_WithSomeInvlaidValues_ExcludesInvalidValues() + { + var inputs = new[] + { + "", + "\"tag\", tag, \"tag\"", + "tag, \"tag\"", + "", + " \"tag ", + "\r\n tag\"\r\n ", + "\"tag会\"", + "\"tag\", \"tag\"", + "W/\"tag\"", + }; + var results = EntityTagHeaderValue.ParseList(inputs); + var expectedResults = new[] + { + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag会\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\"", true), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void ParseStrictList_WithSomeInvlaidValues_Throws() + { + var inputs = new[] + { + "", + "\"tag\", tag, \"tag\"", + "tag, \"tag\"", + "", + " \"tag ", + "\r\n tag\"\r\n ", + "\"tag会\"", + "\"tag\", \"tag\"", + "W/\"tag\"", + }; + Assert.Throws<FormatException>(() => EntityTagHeaderValue.ParseStrictList(inputs)); + } + + [Fact] + public void TryParseList_WithSomeInvlaidValues_ExcludesInvalidValues() + { + var inputs = new[] + { + "", + "\"tag\", tag, \"tag\"", + "tag, \"tag\"", + "", + " \"tag ", + "\r\n tag\"\r\n ", + "\"tag会\"", + "\"tag\", \"tag\"", + "W/\"tag\"", + }; + IList<EntityTagHeaderValue> results; + Assert.True(EntityTagHeaderValue.TryParseList(inputs, out results)); + var expectedResults = new[] + { + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag会\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\""), + new EntityTagHeaderValue("\"tag\"", true), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void TryParseStrictList_WithSomeInvlaidValues_ReturnsFalse() + { + var inputs = new[] + { + "", + "\"tag\", tag, \"tag\"", + "tag, \"tag\"", + "", + " \"tag ", + "\r\n tag\"\r\n ", + "\"tag会\"", + "\"tag\", \"tag\"", + "W/\"tag\"", + }; + IList<EntityTagHeaderValue> results; + Assert.False(EntityTagHeaderValue.TryParseStrictList(inputs, out results)); + } + + private void CheckValidParse(string input, EntityTagHeaderValue expectedResult) + { + var result = EntityTagHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidParse(string input) + { + Assert.Throws<FormatException>(() => EntityTagHeaderValue.Parse(input)); + } + + private void CheckValidTryParse(string input, EntityTagHeaderValue expectedResult) + { + EntityTagHeaderValue result = null; + Assert.True(EntityTagHeaderValue.TryParse(input, out result)); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidTryParse(string input) + { + EntityTagHeaderValue result = null; + Assert.False(EntityTagHeaderValue.TryParse(input, out result)); + Assert.Null(result); + } + + private static void AssertFormatException(string tag) + { + Assert.Throws<FormatException>(() => new EntityTagHeaderValue(tag)); + } + } +} diff --git a/src/Http/Headers/test/HeaderUtilitiesTest.cs b/src/Http/Headers/test/HeaderUtilitiesTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..848190b02e7b22648474081e8a60cea70c6c1c6e --- /dev/null +++ b/src/Http/Headers/test/HeaderUtilitiesTest.cs @@ -0,0 +1,285 @@ +// 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.Globalization; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class HeaderUtilitiesTest + { + private const string Rfc1123Format = "r"; + + [Theory] + [MemberData(nameof(TestValues))] + public void ReturnsSameResultAsRfc1123String(DateTimeOffset dateTime, bool quoted) + { + var formatted = dateTime.ToString(Rfc1123Format); + var expected = quoted ? $"\"{formatted}\"" : formatted; + var actual = HeaderUtilities.FormatDate(dateTime, quoted); + + Assert.Equal(expected, actual); + } + + public static TheoryData<DateTimeOffset, bool> TestValues + { + get + { + var data = new TheoryData<DateTimeOffset, bool>(); + + var date = new DateTimeOffset(new DateTime(2018, 1, 1, 1, 1, 1)); + + foreach (var quoted in new[] { true, false }) + { + data.Add(date, quoted); + + for (var i = 1; i < 60; i++) + { + data.Add(date.AddSeconds(i), quoted); + data.Add(date.AddMinutes(i), quoted); + } + + for (var i = 1; i < DateTime.DaysInMonth(date.Year, date.Month); i++) + { + data.Add(date.AddDays(i), quoted); + } + + for (var i = 1; i < 11; i++) + { + data.Add(date.AddMonths(i), quoted); + } + + for (var i = 1; i < 5; i++) + { + data.Add(date.AddYears(i), quoted); + } + } + + return data; + } + } + + [Theory] + [InlineData("h=1", "h", 1)] + [InlineData("directive1=3, directive2=10", "directive1", 3)] + [InlineData("directive1 =45, directive2=80", "directive1", 45)] + [InlineData("directive1= 89 , directive2=22", "directive1", 89)] + [InlineData("directive1= 89 , directive2= 42", "directive2", 42)] + [InlineData("directive1= 89 , directive= 42", "directive", 42)] + [InlineData("directive1,,,,,directive2 = 42 ", "directive2", 42)] + [InlineData("directive1=;,directive2 = 42 ", "directive2", 42)] + [InlineData("directive1;;,;;,directive2 = 42 ", "directive2", 42)] + [InlineData("directive1=value;q=0.6,directive2 = 42 ", "directive2", 42)] + public void TryParseSeconds_Succeeds(string headerValues, string targetValue, int expectedValue) + { + TimeSpan? value; + Assert.True(HeaderUtilities.TryParseSeconds(new StringValues(headerValues), targetValue, out value)); + Assert.Equal(TimeSpan.FromSeconds(expectedValue), value); + } + + [Theory] + [InlineData("", "")] + [InlineData(null, null)] + [InlineData("h=", "h")] + [InlineData("directive1=, directive2=10", "directive1")] + [InlineData("directive1 , directive2=80", "directive1")] + [InlineData("h=10", "directive")] + [InlineData("directive1", "directive")] + [InlineData("directive1,,,,,,,", "directive")] + [InlineData("h=directive", "directive")] + [InlineData("directive1, directive2=80", "directive")] + [InlineData("directive1=;, directive2=10", "directive1")] + [InlineData("directive1;directive2=10", "directive2")] + public void TryParseSeconds_Fails(string headerValues, string targetValue) + { + TimeSpan? value; + Assert.False(HeaderUtilities.TryParseSeconds(new StringValues(headerValues), targetValue, out value)); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(1234567890)] + [InlineData(long.MaxValue)] + public void FormatNonNegativeInt64_MatchesToString(long value) + { + Assert.Equal(value.ToString(CultureInfo.InvariantCulture), HeaderUtilities.FormatNonNegativeInt64(value)); + } + + [Theory] + [InlineData(-1)] + [InlineData(-1234567890)] + [InlineData(long.MinValue)] + public void FormatNonNegativeInt64_Throws_ForNegativeValues(long value) + { + Assert.Throws<ArgumentOutOfRangeException>(() => HeaderUtilities.FormatNonNegativeInt64(value)); + } + + [Theory] + [InlineData("h", "h", true)] + [InlineData("h=", "h", true)] + [InlineData("h=1", "h", true)] + [InlineData("H", "h", true)] + [InlineData("H=", "h", true)] + [InlineData("H=1", "h", true)] + [InlineData("h", "H", true)] + [InlineData("h=", "H", true)] + [InlineData("h=1", "H", true)] + [InlineData("directive1, directive=10", "directive1", true)] + [InlineData("directive1=, directive=10", "directive1", true)] + [InlineData("directive1=3, directive=10", "directive1", true)] + [InlineData("directive1 , directive=80", "directive1", true)] + [InlineData(" directive1, directive=80", "directive1", true)] + [InlineData("directive1 =45, directive=80", "directive1", true)] + [InlineData("directive1= 89 , directive=22", "directive1", true)] + [InlineData("directive1, directive", "directive", true)] + [InlineData("directive1, directive=", "directive", true)] + [InlineData("directive1, directive=10", "directive", true)] + [InlineData("directive1=3, directive", "directive", true)] + [InlineData("directive1=3, directive=", "directive", true)] + [InlineData("directive1=3, directive=10", "directive", true)] + [InlineData("directive1= 89 , directive= 42", "directive", true)] + [InlineData("directive1= 89 , directive = 42", "directive", true)] + [InlineData("directive1,,,,,directive2 = 42 ", "directive2", true)] + [InlineData("directive1;;,;;,directive2 = 42 ", "directive2", true)] + [InlineData("directive1=;,directive2 = 42 ", "directive2", true)] + [InlineData("directive1=value;q=0.6,directive2 = 42 ", "directive2", true)] + [InlineData(null, null, false)] + [InlineData(null, "", false)] + [InlineData("", null, false)] + [InlineData("", "", false)] + [InlineData("h=10", "directive", false)] + [InlineData("directive1", "directive", false)] + [InlineData("directive1,,,,,,,", "directive", false)] + [InlineData("h=directive", "directive", false)] + [InlineData("directive1, directive2=80", "directive", false)] + [InlineData("directive1;, directive2=80", "directive", false)] + [InlineData("directive1=value;q=0.6;directive2 = 42 ", "directive2", false)] + public void ContainsCacheDirective_MatchesExactValue(string headerValues, string targetValue, bool contains) + { + Assert.Equal(contains, HeaderUtilities.ContainsCacheDirective(new StringValues(headerValues), targetValue)); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData("-1")] + [InlineData("a")] + [InlineData("1.1")] + [InlineData("9223372036854775808")] // long.MaxValue + 1 + public void TryParseNonNegativeInt64_Fails(string valueString) + { + long value = 1; + Assert.False(HeaderUtilities.TryParseNonNegativeInt64(valueString, out value)); + Assert.Equal(0, value); + } + + [Theory] + [InlineData("0", 0)] + [InlineData("9223372036854775807", 9223372036854775807)] // long.MaxValue + public void TryParseNonNegativeInt64_Succeeds(string valueString, long expected) + { + long value = 1; + Assert.True(HeaderUtilities.TryParseNonNegativeInt64(valueString, out value)); + Assert.Equal(expected, value); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData("-1")] + [InlineData("a")] + [InlineData("1.1")] + [InlineData("1,000")] + [InlineData("2147483648")] // int.MaxValue + 1 + public void TryParseNonNegativeInt32_Fails(string valueString) + { + int value = 1; + Assert.False(HeaderUtilities.TryParseNonNegativeInt32(valueString, out value)); + Assert.Equal(0, value); + } + + [Theory] + [InlineData("0", 0)] + [InlineData("2147483647", 2147483647)] // int.MaxValue + public void TryParseNonNegativeInt32_Succeeds(string valueString, long expected) + { + int value = 1; + Assert.True(HeaderUtilities.TryParseNonNegativeInt32(valueString, out value)); + Assert.Equal(expected, value); + } + + [Theory] + [InlineData("\"hello\"", "hello")] + [InlineData("\"hello", "\"hello")] + [InlineData("hello\"", "hello\"")] + [InlineData("\"\"hello\"\"", "\"hello\"")] + public void RemoveQuotes_BehaviorCheck(string input, string expected) + { + var actual = HeaderUtilities.RemoveQuotes(input); + + Assert.Equal(expected, actual); + } + [Theory] + [InlineData("\"hello\"", true)] + [InlineData("\"hello", false)] + [InlineData("hello\"", false)] + [InlineData("\"\"hello\"\"", true)] + public void IsQuoted_BehaviorCheck(string input, bool expected) + { + var actual = HeaderUtilities.IsQuoted(input); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("value", "value")] + [InlineData("\"value\"", "value")] + [InlineData("\"hello\\\\\"", "hello\\")] + [InlineData("\"hello\\\"\"", "hello\"")] + [InlineData("\"hello\\\"foo\\\\bar\\\\baz\\\\\"", "hello\"foo\\bar\\baz\\")] + [InlineData("\"quoted value\"", "quoted value")] + [InlineData("\"quoted\\\"valuewithquote\"", "quoted\"valuewithquote")] + [InlineData("\"hello\\\"", "hello\\")] + public void UnescapeAsQuotedString_BehaviorCheck(string input, string expected) + { + var actual = HeaderUtilities.UnescapeAsQuotedString(input); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("value", "\"value\"")] + [InlineData("23", "\"23\"")] + [InlineData(";;;", "\";;;\"")] + [InlineData("\"value\"", "\"\\\"value\\\"\"")] + [InlineData("unquoted \"value", "\"unquoted \\\"value\"")] + [InlineData("value\\morevalues\\evenmorevalues", "\"value\\\\morevalues\\\\evenmorevalues\"")] + // We have to assume that the input needs to be quoted here + [InlineData("\"\"double quoted string\"\"", "\"\\\"\\\"double quoted string\\\"\\\"\"")] + [InlineData("\t", "\"\t\"")] + public void SetAndEscapeValue_BehaviorCheck(string input, string expected) + { + var actual = HeaderUtilities.EscapeAsQuotedString(input); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("\n")] + [InlineData("\b")] + [InlineData("\r")] + public void SetAndEscapeValue_ControlCharactersThrowFormatException(string input) + { + Assert.Throws<FormatException>(() => { var actual = HeaderUtilities.EscapeAsQuotedString(input); }); + } + + [Fact] + public void SetAndEscapeValue_ThrowsFormatExceptionOnDelCharacter() + { + Assert.Throws<FormatException>(() => { var actual = HeaderUtilities.EscapeAsQuotedString($"{(char)0x7F}"); }); + } + } +} diff --git a/src/Http/Headers/test/MediaTypeHeaderValueComparerTests.cs b/src/Http/Headers/test/MediaTypeHeaderValueComparerTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..3ce2702ec676f96f2be45abe925105254fbdf84c --- /dev/null +++ b/src/Http/Headers/test/MediaTypeHeaderValueComparerTests.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class MediaTypeHeaderValueComparerTests + { + public static IEnumerable<object[]> SortValues + { + get + { + yield return new object[] { + new string[] + { + "application/*", + "text/plain", + "text/*+json;q=0.8", + "text/plain;q=1.0", + "text/plain", + "text/*+json;q=0.6", + "text/plain;q=0", + "*/*;q=0.8", + "*/*;q=1", + "text/*;q=1", + "text/plain;q=0.8", + "text/*;q=0.8", + "text/*;q=0.6", + "text/*+json;q=0.4", + "text/*;q=1.0", + "*/*;q=0.4", + "text/plain;q=0.6", + "text/xml", + }, + new string[] + { + "text/plain", + "text/plain;q=1.0", + "text/plain", + "text/xml", + "application/*", + "text/*;q=1", + "text/*;q=1.0", + "*/*;q=1", + "text/plain;q=0.8", + "text/*+json;q=0.8", + "text/*;q=0.8", + "*/*;q=0.8", + "text/plain;q=0.6", + "text/*+json;q=0.6", + "text/*;q=0.6", + "text/*+json;q=0.4", + "*/*;q=0.4", + "text/plain;q=0", + } + }; + } + } + + [Theory] + [MemberData(nameof(SortValues))] + public void SortMediaTypeHeaderValuesByQFactor_SortsCorrectly(IEnumerable<string> unsorted, IEnumerable<string> expectedSorted) + { + var unsortedValues = MediaTypeHeaderValue.ParseList(unsorted.ToList()); + var expectedSortedValues = MediaTypeHeaderValue.ParseList(expectedSorted.ToList()); + + var actualSorted = unsortedValues.OrderByDescending(m => m, MediaTypeHeaderValueComparer.QualityComparer).ToList(); + + Assert.Equal(expectedSortedValues, actualSorted); + } + } +} diff --git a/src/Http/Headers/test/MediaTypeHeaderValueTest.cs b/src/Http/Headers/test/MediaTypeHeaderValueTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..75cccabc9c1d40ee8c22b93378b9e4590bbf5fbc --- /dev/null +++ b/src/Http/Headers/test/MediaTypeHeaderValueTest.cs @@ -0,0 +1,847 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class MediaTypeHeaderValueTest + { + [Fact] + public void Ctor_MediaTypeNull_Throw() + { + Assert.Throws<ArgumentException>(() => new MediaTypeHeaderValue(null)); + // null and empty should be treated the same. So we also throw for empty strings. + Assert.Throws<ArgumentException>(() => new MediaTypeHeaderValue(string.Empty)); + } + + [Fact] + public void Ctor_MediaTypeInvalidFormat_ThrowFormatException() + { + // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. + AssertFormatException(" text/plain "); + AssertFormatException("text / plain"); + AssertFormatException("text/ plain"); + AssertFormatException("text /plain"); + AssertFormatException("text/plain "); + AssertFormatException(" text/plain"); + AssertFormatException("te xt/plain"); + AssertFormatException("te=xt/plain"); + AssertFormatException("teäxt/plain"); + AssertFormatException("text/pläin"); + AssertFormatException("text"); + AssertFormatException("\"text/plain\""); + AssertFormatException("text/plain; charset=utf-8; "); + AssertFormatException("text/plain;"); + AssertFormatException("text/plain;charset=utf-8"); // ctor takes only media-type name, no parameters + } + + public static TheoryData<string, string, string> MediaTypesWithSuffixes => + new TheoryData<string, string, string> + { + // See https://tools.ietf.org/html/rfc6838#section-4.2 for allowed names spec + { "application/json", "json", null }, + { "application/json+", "json", "" }, + { "application/+json", "", "json" }, + { "application/entitytype+json", "entitytype", "json" }, + { "applica+tion/entitytype+json", "entitytype", "json" }, + }; + + [Theory] + [MemberData(nameof(MediaTypesWithSuffixes))] + public void Ctor_CanParseSuffixedMediaTypes(string mediaType, string expectedSubTypeWithoutSuffix, string expectedSubTypeSuffix) + { + var result = new MediaTypeHeaderValue(mediaType); + + Assert.Equal(new StringSegment(expectedSubTypeWithoutSuffix), result.SubTypeWithoutSuffix); // TODO consider overloading to have SubTypeWithoutSuffix? + Assert.Equal(new StringSegment(expectedSubTypeSuffix), result.Suffix); + } + + public static TheoryData<string, string, string> MediaTypesWithSuffixesAndSpaces => + new TheoryData<string, string, string> + { + // See https://tools.ietf.org/html/rfc6838#section-4.2 for allowed names spec + { " application / json+xml", "json", "xml" }, + { " application / vnd.com-pany.some+entity!.v2+js.#$&^_n ; q=\"0.3+1\"", "vnd.com-pany.some+entity!.v2", "js.#$&^_n"}, + { " application/ +json", "", "json" }, + { " application/ entitytype+json ", "entitytype", "json" }, + { " applica+tion/ entitytype+json ", "entitytype", "json" } + }; + + [Theory] + [MemberData(nameof(MediaTypesWithSuffixesAndSpaces))] + public void Parse_CanParseSuffixedMediaTypes(string mediaType, string expectedSubTypeWithoutSuffix, string expectedSubTypeSuffix) + { + var result = MediaTypeHeaderValue.Parse(mediaType); + + Assert.Equal(new StringSegment(expectedSubTypeWithoutSuffix), result.SubTypeWithoutSuffix); // TODO consider overloading to have SubTypeWithoutSuffix? + Assert.Equal(new StringSegment(expectedSubTypeSuffix), result.Suffix); + } + + [Theory] + [InlineData("*/*", true)] + [InlineData("text/*", true)] + [InlineData("text/*+suffix", true)] + [InlineData("text/*+", true)] + [InlineData("text/*+*", true)] + [InlineData("text/json+suffix", false)] + [InlineData("*/json+*", false)] + public void MatchesAllSubTypesWithoutSuffix_ReturnsExpectedResult(string value, bool expectedReturnValue) + { + // Arrange + var mediaType = new MediaTypeHeaderValue(value); + + // Act + var result = mediaType.MatchesAllSubTypesWithoutSuffix; + + // Assert + Assert.Equal(expectedReturnValue, result); + } + + [Fact] + public void Ctor_MediaTypeValidFormat_SuccessfullyCreated() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + Assert.Equal("text/plain", mediaType.MediaType); + Assert.Equal(0, mediaType.Parameters.Count); + Assert.Null(mediaType.Charset.Value); + } + + [Fact] + public void Ctor_AddNameAndQuality_QualityParameterAdded() + { + var mediaType = new MediaTypeHeaderValue("application/xml", 0.08); + Assert.Equal(0.08, mediaType.Quality); + Assert.Equal("application/xml", mediaType.MediaType); + Assert.Equal(1, mediaType.Parameters.Count); + } + + [Fact] + public void Parameters_AddNull_Throw() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + Assert.Throws<ArgumentNullException>(() => mediaType.Parameters.Add(null)); + } + + [Fact] + public void Copy_SimpleMediaType_Copied() + { + var mediaType0 = new MediaTypeHeaderValue("text/plain"); + var mediaType1 = mediaType0.Copy(); + Assert.NotSame(mediaType0, mediaType1); + Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); + Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); + Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); + } + + [Fact] + public void CopyAsReadOnly_SimpleMediaType_CopiedAndReadOnly() + { + var mediaType0 = new MediaTypeHeaderValue("text/plain"); + var mediaType1 = mediaType0.CopyAsReadOnly(); + Assert.NotSame(mediaType0, mediaType1); + Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); + Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); + Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); + + Assert.False(mediaType0.IsReadOnly); + Assert.True(mediaType1.IsReadOnly); + Assert.Throws<InvalidOperationException>(() => { mediaType1.MediaType = "some/value"; }); + } + + [Fact] + public void Copy_WithParameters_Copied() + { + var mediaType0 = new MediaTypeHeaderValue("text/plain"); + mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value")); + var mediaType1 = mediaType0.Copy(); + Assert.NotSame(mediaType0, mediaType1); + Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); + Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); + Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); + var pair0 = mediaType0.Parameters.First(); + var pair1 = mediaType1.Parameters.First(); + Assert.NotSame(pair0, pair1); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); + } + + [Fact] + public void CopyAsReadOnly_WithParameters_CopiedAndReadOnly() + { + var mediaType0 = new MediaTypeHeaderValue("text/plain"); + mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value")); + var mediaType1 = mediaType0.CopyAsReadOnly(); + Assert.NotSame(mediaType0, mediaType1); + Assert.False(mediaType0.IsReadOnly); + Assert.True(mediaType1.IsReadOnly); + Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); + + Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); + Assert.False(mediaType0.Parameters.IsReadOnly); + Assert.True(mediaType1.Parameters.IsReadOnly); + Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); + Assert.Throws<NotSupportedException>(() => mediaType1.Parameters.Add(new NameValueHeaderValue("name"))); + Assert.Throws<NotSupportedException>(() => mediaType1.Parameters.Remove(new NameValueHeaderValue("name"))); + Assert.Throws<NotSupportedException>(() => mediaType1.Parameters.Clear()); + + var pair0 = mediaType0.Parameters.First(); + var pair1 = mediaType1.Parameters.First(); + Assert.NotSame(pair0, pair1); + Assert.False(pair0.IsReadOnly); + Assert.True(pair1.IsReadOnly); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); + } + + [Fact] + public void CopyFromReadOnly_WithParameters_CopiedAsNonReadOnly() + { + var mediaType0 = new MediaTypeHeaderValue("text/plain"); + mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value")); + var mediaType1 = mediaType0.CopyAsReadOnly(); + var mediaType2 = mediaType1.Copy(); + + Assert.NotSame(mediaType2, mediaType1); + Assert.Same(mediaType2.MediaType.Value, mediaType1.MediaType.Value); + Assert.True(mediaType1.IsReadOnly); + Assert.False(mediaType2.IsReadOnly); + Assert.NotSame(mediaType2.Parameters, mediaType1.Parameters); + Assert.Equal(mediaType2.Parameters.Count, mediaType1.Parameters.Count); + var pair2 = mediaType2.Parameters.First(); + var pair1 = mediaType1.Parameters.First(); + Assert.NotSame(pair2, pair1); + Assert.True(pair1.IsReadOnly); + Assert.False(pair2.IsReadOnly); + Assert.Same(pair2.Name.Value, pair1.Name.Value); + Assert.Same(pair2.Value.Value, pair1.Value.Value); + } + + [Fact] + public void MediaType_SetAndGetMediaType_MatchExpectations() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + Assert.Equal("text/plain", mediaType.MediaType); + + mediaType.MediaType = "application/xml"; + Assert.Equal("application/xml", mediaType.MediaType); + } + + [Fact] + public void Charset_SetCharsetAndValidateObject_ParametersEntryForCharsetAdded() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + mediaType.Charset = "mycharset"; + Assert.Equal("mycharset", mediaType.Charset); + Assert.Equal(1, mediaType.Parameters.Count); + Assert.Equal("charset", mediaType.Parameters.First().Name); + + mediaType.Charset = null; + Assert.Null(mediaType.Charset.Value); + Assert.Equal(0, mediaType.Parameters.Count); + mediaType.Charset = null; // It's OK to set it again to null; no exception. + } + + [Fact] + public void Charset_AddCharsetParameterThenUseProperty_ParametersEntryIsOverwritten() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var charset = new NameValueHeaderValue("CHARSET", "old_charset"); + mediaType.Parameters.Add(charset); + Assert.Equal(1, mediaType.Parameters.Count); + Assert.Equal("CHARSET", mediaType.Parameters.First().Name); + + mediaType.Charset = "new_charset"; + Assert.Equal("new_charset", mediaType.Charset); + Assert.Equal(1, mediaType.Parameters.Count); + Assert.Equal("CHARSET", mediaType.Parameters.First().Name); + + mediaType.Parameters.Remove(charset); + Assert.Null(mediaType.Charset.Value); + } + + [Fact] + public void Quality_SetCharsetAndValidateObject_ParametersEntryForCharsetAdded() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + mediaType.Quality = 0.563156454; + Assert.Equal(0.563, mediaType.Quality); + Assert.Equal(1, mediaType.Parameters.Count); + Assert.Equal("q", mediaType.Parameters.First().Name); + Assert.Equal("0.563", mediaType.Parameters.First().Value); + + mediaType.Quality = null; + Assert.Null(mediaType.Quality); + Assert.Equal(0, mediaType.Parameters.Count); + mediaType.Quality = null; // It's OK to set it again to null; no exception. + } + + [Fact] + public void Quality_AddQualityParameterThenUseProperty_ParametersEntryIsOverwritten() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + + var quality = new NameValueHeaderValue("q", "0.132"); + mediaType.Parameters.Add(quality); + Assert.Equal(1, mediaType.Parameters.Count); + Assert.Equal("q", mediaType.Parameters.First().Name); + Assert.Equal(0.132, mediaType.Quality); + + mediaType.Quality = 0.9; + Assert.Equal(0.9, mediaType.Quality); + Assert.Equal(1, mediaType.Parameters.Count); + Assert.Equal("q", mediaType.Parameters.First().Name); + + mediaType.Parameters.Remove(quality); + Assert.Null(mediaType.Quality); + } + + [Fact] + public void Quality_AddQualityParameterUpperCase_CaseInsensitiveComparison() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + + var quality = new NameValueHeaderValue("Q", "0.132"); + mediaType.Parameters.Add(quality); + Assert.Equal(1, mediaType.Parameters.Count); + Assert.Equal("Q", mediaType.Parameters.First().Name); + Assert.Equal(0.132, mediaType.Quality); + } + + [Fact] + public void Quality_LessThanZero_Throw() + { + Assert.Throws<ArgumentOutOfRangeException>(() => new MediaTypeHeaderValue("application/xml", -0.01)); + } + + [Fact] + public void Quality_GreaterThanOne_Throw() + { + var mediaType = new MediaTypeHeaderValue("application/xml"); + Assert.Throws<ArgumentOutOfRangeException>(() => mediaType.Quality = 1.01); + } + + [Fact] + public void ToString_UseDifferentMediaTypes_AllSerializedCorrectly() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + Assert.Equal("text/plain", mediaType.ToString()); + + mediaType.Charset = "utf-8"; + Assert.Equal("text/plain; charset=utf-8", mediaType.ToString()); + + mediaType.Parameters.Add(new NameValueHeaderValue("custom", "\"custom value\"")); + Assert.Equal("text/plain; charset=utf-8; custom=\"custom value\"", mediaType.ToString()); + + mediaType.Charset = null; + Assert.Equal("text/plain; custom=\"custom value\"", mediaType.ToString()); + } + + [Fact] + public void GetHashCode_UseMediaTypeWithAndWithoutParameters_SameOrDifferentHashCodes() + { + var mediaType1 = new MediaTypeHeaderValue("text/plain"); + var mediaType2 = new MediaTypeHeaderValue("text/plain"); + mediaType2.Charset = "utf-8"; + var mediaType3 = new MediaTypeHeaderValue("text/plain"); + mediaType3.Parameters.Add(new NameValueHeaderValue("name", "value")); + var mediaType4 = new MediaTypeHeaderValue("TEXT/plain"); + var mediaType5 = new MediaTypeHeaderValue("TEXT/plain"); + mediaType5.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8")); + + Assert.NotEqual(mediaType1.GetHashCode(), mediaType2.GetHashCode()); + Assert.NotEqual(mediaType1.GetHashCode(), mediaType3.GetHashCode()); + Assert.NotEqual(mediaType2.GetHashCode(), mediaType3.GetHashCode()); + Assert.Equal(mediaType1.GetHashCode(), mediaType4.GetHashCode()); + Assert.Equal(mediaType2.GetHashCode(), mediaType5.GetHashCode()); + } + + [Fact] + public void Equals_UseMediaTypeWithAndWithoutParameters_EqualOrNotEqualNoExceptions() + { + var mediaType1 = new MediaTypeHeaderValue("text/plain"); + var mediaType2 = new MediaTypeHeaderValue("text/plain"); + mediaType2.Charset = "utf-8"; + var mediaType3 = new MediaTypeHeaderValue("text/plain"); + mediaType3.Parameters.Add(new NameValueHeaderValue("name", "value")); + var mediaType4 = new MediaTypeHeaderValue("TEXT/plain"); + var mediaType5 = new MediaTypeHeaderValue("TEXT/plain"); + mediaType5.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8")); + var mediaType6 = new MediaTypeHeaderValue("TEXT/plain"); + mediaType6.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8")); + mediaType6.Parameters.Add(new NameValueHeaderValue("custom", "value")); + var mediaType7 = new MediaTypeHeaderValue("text/other"); + + Assert.False(mediaType1.Equals(mediaType2), "No params vs. charset."); + Assert.False(mediaType2.Equals(mediaType1), "charset vs. no params."); + Assert.False(mediaType1.Equals(null), "No params vs. <null>."); + Assert.False(mediaType1.Equals(mediaType3), "No params vs. custom param."); + Assert.False(mediaType2.Equals(mediaType3), "charset vs. custom param."); + Assert.True(mediaType1.Equals(mediaType4), "Different casing."); + Assert.True(mediaType2.Equals(mediaType5), "Different casing in charset."); + Assert.False(mediaType5.Equals(mediaType6), "charset vs. custom param."); + Assert.False(mediaType1.Equals(mediaType7), "text/plain vs. text/other."); + } + + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidParse("\r\n text/plain ", new MediaTypeHeaderValue("text/plain")); + CheckValidParse("text/plain", new MediaTypeHeaderValue("text/plain")); + + CheckValidParse("\r\n text / plain ; charset = utf-8 ", new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }); + CheckValidParse(" text/plain;charset=utf-8", new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }); + + CheckValidParse("text/plain; charset=iso-8859-1", new MediaTypeHeaderValue("text/plain") { Charset = "iso-8859-1" }); + + var expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }; + expected.Parameters.Add(new NameValueHeaderValue("custom", "value")); + CheckValidParse(" text/plain; custom=value;charset=utf-8", expected); + + expected = new MediaTypeHeaderValue("text/plain"); + expected.Parameters.Add(new NameValueHeaderValue("custom")); + CheckValidParse(" text/plain; custom", expected); + + expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }; + expected.Parameters.Add(new NameValueHeaderValue("custom", "\"x\"")); + CheckValidParse("text / plain ; custom =\r\n \"x\" ; charset = utf-8 ", expected); + + expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }; + expected.Parameters.Add(new NameValueHeaderValue("custom", "\"x\"")); + CheckValidParse("text/plain;custom=\"x\";charset=utf-8", expected); + + expected = new MediaTypeHeaderValue("text/plain"); + CheckValidParse("text/plain;", expected); + + expected = new MediaTypeHeaderValue("text/plain"); + expected.Parameters.Add(new NameValueHeaderValue("name", "")); + CheckValidParse("text/plain;name=", expected); + + expected = new MediaTypeHeaderValue("text/plain"); + expected.Parameters.Add(new NameValueHeaderValue("name", "value")); + CheckValidParse("text/plain;name=value;", expected); + + expected = new MediaTypeHeaderValue("text/plain"); + expected.Charset = "iso-8859-1"; + expected.Quality = 1.0; + CheckValidParse("text/plain; charset=iso-8859-1; q=1.0", expected); + + expected = new MediaTypeHeaderValue("*/xml"); + expected.Charset = "utf-8"; + expected.Quality = 0.5; + CheckValidParse("\r\n */xml; charset=utf-8; q=0.5", expected); + + expected = new MediaTypeHeaderValue("*/*"); + CheckValidParse("*/*", expected); + + expected = new MediaTypeHeaderValue("text/*"); + expected.Charset = "utf-8"; + expected.Parameters.Add(new NameValueHeaderValue("foo", "bar")); + CheckValidParse("text/*; charset=utf-8; foo=bar", expected); + + expected = new MediaTypeHeaderValue("text/plain"); + expected.Charset = "utf-8"; + expected.Quality = 0; + expected.Parameters.Add(new NameValueHeaderValue("foo", "bar")); + CheckValidParse("text/plain; charset=utf-8; foo=bar; q=0.0", expected); + } + + [Fact] + public void Parse_SetOfInvalidValueStrings_Throws() + { + CheckInvalidParse(""); + CheckInvalidParse(" "); + CheckInvalidParse(null); + CheckInvalidParse("text/plain会"); + CheckInvalidParse("text/plain ,"); + CheckInvalidParse("text/plain,"); + CheckInvalidParse("text/plain; charset=utf-8 ,"); + CheckInvalidParse("text/plain; charset=utf-8,"); + CheckInvalidParse("textplain"); + CheckInvalidParse("text/"); + CheckInvalidParse(",, , ,,text/plain; charset=iso-8859-1; q=1.0,\r\n */xml; charset=utf-8; q=0.5,,,"); + CheckInvalidParse("text/plain; charset=iso-8859-1; q=1.0, */xml; charset=utf-8; q=0.5"); + CheckInvalidParse(" , */xml; charset=utf-8; q=0.5 "); + CheckInvalidParse("text/plain; charset=iso-8859-1; q=1.0 , "); + } + + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + var expected = new MediaTypeHeaderValue("text/plain"); + CheckValidTryParse("\r\n text/plain ", expected); + CheckValidTryParse("text/plain", expected); + + // We don't have to test all possible input strings, since most of the pieces are handled by other parsers. + // The purpose of this test is to verify that these other parsers are combined correctly to build a + // media-type parser. + expected.Charset = "utf-8"; + CheckValidTryParse("\r\n text / plain ; charset = utf-8 ", expected); + CheckValidTryParse(" text/plain;charset=utf-8", expected); + + var value1 = new MediaTypeHeaderValue("text/plain"); + value1.Charset = "iso-8859-1"; + value1.Quality = 1.0; + + CheckValidTryParse("text/plain; charset=iso-8859-1; q=1.0", value1); + + var value2 = new MediaTypeHeaderValue("*/xml"); + value2.Charset = "utf-8"; + value2.Quality = 0.5; + + CheckValidTryParse("\r\n */xml; charset=utf-8; q=0.5", value2); + } + + [Fact] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() + { + CheckInvalidTryParse(""); + CheckInvalidTryParse(" "); + CheckInvalidTryParse(null); + CheckInvalidTryParse("text/plain会"); + CheckInvalidTryParse("text/plain ,"); + CheckInvalidTryParse("text/plain,"); + CheckInvalidTryParse("text/plain; charset=utf-8 ,"); + CheckInvalidTryParse("text/plain; charset=utf-8,"); + CheckInvalidTryParse("textplain"); + CheckInvalidTryParse("text/"); + CheckInvalidTryParse(",, , ,,text/plain; charset=iso-8859-1; q=1.0,\r\n */xml; charset=utf-8; q=0.5,,,"); + CheckInvalidTryParse("text/plain; charset=iso-8859-1; q=1.0, */xml; charset=utf-8; q=0.5"); + CheckInvalidTryParse(" , */xml; charset=utf-8; q=0.5 "); + CheckInvalidTryParse("text/plain; charset=iso-8859-1; q=1.0 , "); + } + + [Fact] + public void ParseList_NullOrEmptyArray_ReturnsEmptyList() + { + var results = MediaTypeHeaderValue.ParseList(null); + Assert.NotNull(results); + Assert.Equal(0, results.Count); + + results = MediaTypeHeaderValue.ParseList(new string[0]); + Assert.NotNull(results); + Assert.Equal(0, results.Count); + + results = MediaTypeHeaderValue.ParseList(new string[] { "" }); + Assert.NotNull(results); + Assert.Equal(0, results.Count); + } + + [Fact] + public void TryParseList_NullOrEmptyArray_ReturnsFalse() + { + IList<MediaTypeHeaderValue> results; + Assert.False(MediaTypeHeaderValue.TryParseList(null, out results)); + Assert.False(MediaTypeHeaderValue.TryParseList(new string[0], out results)); + Assert.False(MediaTypeHeaderValue.TryParseList(new string[] { "" }, out results)); + } + + [Fact] + public void ParseList_SetOfValidValueStrings_ReturnsValues() + { + var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" }; + var results = MediaTypeHeaderValue.ParseList(inputs); + + var expectedResults = new[] + { + new MediaTypeHeaderValue("text/html"), + new MediaTypeHeaderValue("application/xhtml+xml"), + new MediaTypeHeaderValue("application/xml", 0.9), + new MediaTypeHeaderValue("image/webp"), + new MediaTypeHeaderValue("*/*", 0.8), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void ParseStrictList_SetOfValidValueStrings_ReturnsValues() + { + var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" }; + var results = MediaTypeHeaderValue.ParseStrictList(inputs); + + var expectedResults = new[] + { + new MediaTypeHeaderValue("text/html"), + new MediaTypeHeaderValue("application/xhtml+xml"), + new MediaTypeHeaderValue("application/xml", 0.9), + new MediaTypeHeaderValue("image/webp"), + new MediaTypeHeaderValue("*/*", 0.8), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void TryParseList_SetOfValidValueStrings_ReturnsTrue() + { + var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" }; + IList<MediaTypeHeaderValue> results; + Assert.True(MediaTypeHeaderValue.TryParseList(inputs, out results)); + + var expectedResults = new[] + { + new MediaTypeHeaderValue("text/html"), + new MediaTypeHeaderValue("application/xhtml+xml"), + new MediaTypeHeaderValue("application/xml", 0.9), + new MediaTypeHeaderValue("image/webp"), + new MediaTypeHeaderValue("*/*", 0.8), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void TryParseStrictList_SetOfValidValueStrings_ReturnsTrue() + { + var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" }; + IList<MediaTypeHeaderValue> results; + Assert.True(MediaTypeHeaderValue.TryParseStrictList(inputs, out results)); + + var expectedResults = new[] + { + new MediaTypeHeaderValue("text/html"), + new MediaTypeHeaderValue("application/xhtml+xml"), + new MediaTypeHeaderValue("application/xml", 0.9), + new MediaTypeHeaderValue("image/webp"), + new MediaTypeHeaderValue("*/*", 0.8), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void ParseList_WithSomeInvlaidValues_IgnoresInvalidValues() + { + var inputs = new[] + { + "text/html,application/xhtml+xml, ignore-this, ignore/this", + "application/xml;q=0.9,image/webp,*/*;q=0.8" + }; + var results = MediaTypeHeaderValue.ParseList(inputs); + + var expectedResults = new[] + { + new MediaTypeHeaderValue("text/html"), + new MediaTypeHeaderValue("application/xhtml+xml"), + new MediaTypeHeaderValue("ignore/this"), + new MediaTypeHeaderValue("application/xml", 0.9), + new MediaTypeHeaderValue("image/webp"), + new MediaTypeHeaderValue("*/*", 0.8), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void ParseStrictList_WithSomeInvlaidValues_Throws() + { + var inputs = new[] + { + "text/html,application/xhtml+xml, ignore-this, ignore/this", + "application/xml;q=0.9,image/webp,*/*;q=0.8" + }; + Assert.Throws<FormatException>(() => MediaTypeHeaderValue.ParseStrictList(inputs)); + } + + [Fact] + public void TryParseList_WithSomeInvlaidValues_IgnoresInvalidValues() + { + var inputs = new[] + { + "text/html,application/xhtml+xml, ignore-this, ignore/this", + "application/xml;q=0.9,image/webp,*/*;q=0.8", + "application/xml;q=0 4" + }; + IList<MediaTypeHeaderValue> results; + Assert.True(MediaTypeHeaderValue.TryParseList(inputs, out results)); + + var expectedResults = new[] + { + new MediaTypeHeaderValue("text/html"), + new MediaTypeHeaderValue("application/xhtml+xml"), + new MediaTypeHeaderValue("ignore/this"), + new MediaTypeHeaderValue("application/xml", 0.9), + new MediaTypeHeaderValue("image/webp"), + new MediaTypeHeaderValue("*/*", 0.8), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void TryParseStrictList_WithSomeInvlaidValues_ReturnsFalse() + { + var inputs = new[] + { + "text/html,application/xhtml+xml, ignore-this, ignore/this", + "application/xml;q=0.9,image/webp,*/*;q=0.8", + "application/xml;q=0 4" + }; + IList<MediaTypeHeaderValue> results; + Assert.False(MediaTypeHeaderValue.TryParseStrictList(inputs, out results)); + } + + [Theory] + [InlineData("*/*;", "*/*")] + [InlineData("text/*", "text/*")] + [InlineData("text/*;", "*/*")] + [InlineData("text/plain;", "text/plain")] + [InlineData("text/plain", "text/*")] + [InlineData("text/plain;", "*/*")] + [InlineData("*/*;missingparam=4", "*/*")] + [InlineData("text/*;missingparam=4;", "*/*;")] + [InlineData("text/plain;missingparam=4", "*/*;")] + [InlineData("text/plain;missingparam=4", "text/*")] + [InlineData("text/plain;charset=utf-8", "text/plain;charset=utf-8")] + [InlineData("text/plain;version=v1", "Text/plain;Version=v1")] + [InlineData("text/plain;version=v1", "tExT/plain;version=V1")] + [InlineData("text/plain;version=v1", "TEXT/PLAIN;VERSION=V1")] + [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;charset=utf-8;foo=bar;q=0.0")] + [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;foo=bar;q=0.0;charset=utf-8")] // different order of parameters + [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/*;charset=utf-8;foo=bar;q=0.0")] + [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "*/*;charset=utf-8;foo=bar;q=0.0")] + [InlineData("application/json;v=2", "application/json;*")] + [InlineData("application/json;v=2;charset=utf-8", "application/json;v=2;*")] + public void IsSubsetOf_PositiveCases(string mediaType1, string mediaType2) + { + // Arrange + var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1); + var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2); + + // Act + var isSubset = parsedMediaType1.IsSubsetOf(parsedMediaType2); + + // Assert + Assert.True(isSubset); + } + + [Theory] + [InlineData("application/html", "text/*")] + [InlineData("application/json", "application/html")] + [InlineData("text/plain;version=v1", "text/plain;version=")] + [InlineData("*/*;", "text/plain;charset=utf-8;foo=bar;q=0.0")] + [InlineData("text/*;", "text/plain;charset=utf-8;foo=bar;q=0.0")] + [InlineData("text/*;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")] + [InlineData("*/*;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")] + [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")] + [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/*;missingparam=4;")] + [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "*/*;missingparam=4;")] + public void IsSubsetOf_NegativeCases(string mediaType1, string mediaType2) + { + // Arrange + var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1); + var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2); + + // Act + var isSubset = parsedMediaType1.IsSubsetOf(parsedMediaType2); + + // Assert + Assert.False(isSubset); + } + + [Theory] + [InlineData("application/entity+json", "application/entity+json")] + [InlineData("application/*+json", "application/entity+json")] + [InlineData("application/*+json", "application/*+json")] + [InlineData("application/*", "application/*+JSON")] + [InlineData("application/vnd.github+json", "application/vnd.github+json")] + [InlineData("application/*", "application/entity+JSON")] + [InlineData("*/*", "application/entity+json")] + public void IsSubsetOfWithSuffixes_PositiveCases(string set, string subset) + { + // Arrange + var setMediaType = MediaTypeHeaderValue.Parse(set); + var subSetMediaType = MediaTypeHeaderValue.Parse(subset); + + // Act + var result = subSetMediaType.IsSubsetOf(setMediaType); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("application/entity+json", "application/entity+txt")] + [InlineData("application/entity+json", "application/entity.v2+json")] + [InlineData("application/*+json", "application/entity+txt")] + [InlineData("application/*+*", "application/json")] + [InlineData("application/entity+*", "application/entity+json")] // We don't allow suffixes to be wildcards + [InlineData("application/*+*", "application/entity+json")] // We don't allow suffixes to be wildcards + public void IsSubSetOfWithSuffixes_NegativeCases(string set, string subset) + { + // Arrange + var setMediaType = MediaTypeHeaderValue.Parse(set); + var subSetMediaType = MediaTypeHeaderValue.Parse(subset); + + // Act + var result = subSetMediaType.IsSubsetOf(setMediaType); + + // Assert + Assert.False(result); + } + + public static TheoryData<string, List<StringSegment>> MediaTypesWithFacets => + new TheoryData<string, List<StringSegment>> + { + { "application/vdn.github", + new List<StringSegment>(){ "vdn", "github" } }, + { "application/vdn.github+json", + new List<StringSegment>(){ "vdn", "github" } }, + { "application/vdn.github.v3+json", + new List<StringSegment>(){ "vdn", "github", "v3" } }, + { "application/vdn.github.+json", + new List<StringSegment>(){ "vdn", "github", "" } }, + }; + + [Theory] + [MemberData(nameof(MediaTypesWithFacets))] + public void Facets_TestPositiveCases(string input, List<StringSegment> expected) + { + // Arrange + var mediaType = MediaTypeHeaderValue.Parse(input); + + // Act + var result = mediaType.Facets; + + // Assert + Assert.Equal(expected, result); + } + + private void CheckValidParse(string input, MediaTypeHeaderValue expectedResult) + { + var result = MediaTypeHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidParse(string input) + { + Assert.Throws<FormatException>(() => MediaTypeHeaderValue.Parse(input)); + } + + private void CheckValidTryParse(string input, MediaTypeHeaderValue expectedResult) + { + MediaTypeHeaderValue result = null; + Assert.True(MediaTypeHeaderValue.TryParse(input, out result)); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidTryParse(string input) + { + MediaTypeHeaderValue result = null; + Assert.False(MediaTypeHeaderValue.TryParse(input, out result)); + Assert.Null(result); + } + + private static void AssertFormatException(string mediaType) + { + Assert.Throws<FormatException>(() => new MediaTypeHeaderValue(mediaType)); + } + } +} diff --git a/src/Http/Headers/test/Microsoft.Net.Http.Headers.Tests.csproj b/src/Http/Headers/test/Microsoft.Net.Http.Headers.Tests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..eb53233e33be88e61e93b6f7a425f0182c332943 --- /dev/null +++ b/src/Http/Headers/test/Microsoft.Net.Http.Headers.Tests.csproj @@ -0,0 +1,11 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.Net.Http.Headers" /> + </ItemGroup> + +</Project> diff --git a/src/Http/Headers/test/NameValueHeaderValueTest.cs b/src/Http/Headers/test/NameValueHeaderValueTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..cac18debbbd2e0f6cf9505690e752ab09b8eb160 --- /dev/null +++ b/src/Http/Headers/test/NameValueHeaderValueTest.cs @@ -0,0 +1,699 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class NameValueHeaderValueTest + { + [Fact] + public void Ctor_NameNull_Throw() + { + Assert.Throws<ArgumentException>(() => new NameValueHeaderValue(null)); + // null and empty should be treated the same. So we also throw for empty strings. + Assert.Throws<ArgumentException>(() => new NameValueHeaderValue(string.Empty)); + } + + [Fact] + public void Ctor_NameInvalidFormat_ThrowFormatException() + { + // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. + AssertFormatException(" text ", null); + AssertFormatException("text ", null); + AssertFormatException(" text", null); + AssertFormatException("te xt", null); + AssertFormatException("te=xt", null); // The ctor takes a name which must not contain '='. + AssertFormatException("teäxt", null); + } + + [Fact] + public void Ctor_NameValidFormat_SuccessfullyCreated() + { + var nameValue = new NameValueHeaderValue("text", null); + Assert.Equal("text", nameValue.Name); + } + + [Fact] + public void Ctor_ValueInvalidFormat_ThrowFormatException() + { + // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. + AssertFormatException("text", " token "); + AssertFormatException("text", "token "); + AssertFormatException("text", " token"); + AssertFormatException("text", "token string"); + AssertFormatException("text", "\"quoted string with \" quotes\""); + AssertFormatException("text", "\"quoted string with \"two\" quotes\""); + } + + [Fact] + public void Ctor_ValueValidFormat_SuccessfullyCreated() + { + CheckValue(null); + CheckValue(string.Empty); + CheckValue("token_string"); + CheckValue("\"quoted string\""); + CheckValue("\"quoted string with quoted \\\" quote-pair\""); + } + + [Fact] + public void Copy_NameOnly_SuccesfullyCopied() + { + var pair0 = new NameValueHeaderValue("name"); + var pair1 = pair0.Copy(); + Assert.NotSame(pair0, pair1); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Null(pair0.Value.Value); + Assert.Null(pair1.Value.Value); + + // Change one value and verify the other is unchanged. + pair0.Value = "othervalue"; + Assert.Equal("othervalue", pair0.Value); + Assert.Null(pair1.Value.Value); + } + + [Fact] + public void CopyAsReadOnly_NameOnly_CopiedAndReadOnly() + { + var pair0 = new NameValueHeaderValue("name"); + var pair1 = pair0.CopyAsReadOnly(); + Assert.NotSame(pair0, pair1); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Null(pair0.Value.Value); + Assert.Null(pair1.Value.Value); + Assert.False(pair0.IsReadOnly); + Assert.True(pair1.IsReadOnly); + + // Change one value and verify the other is unchanged. + pair0.Value = "othervalue"; + Assert.Equal("othervalue", pair0.Value); + Assert.Null(pair1.Value.Value); + Assert.Throws<InvalidOperationException>(() => { pair1.Value = "othervalue"; }); + } + + [Fact] + public void Copy_NameAndValue_SuccesfullyCopied() + { + var pair0 = new NameValueHeaderValue("name", "value"); + var pair1 = pair0.Copy(); + Assert.NotSame(pair0, pair1); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); + + // Change one value and verify the other is unchanged. + pair0.Value = "othervalue"; + Assert.Equal("othervalue", pair0.Value); + Assert.Equal("value", pair1.Value); + } + + [Fact] + public void CopyAsReadOnly_NameAndValue_CopiedAndReadOnly() + { + var pair0 = new NameValueHeaderValue("name", "value"); + var pair1 = pair0.CopyAsReadOnly(); + Assert.NotSame(pair0, pair1); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); + Assert.False(pair0.IsReadOnly); + Assert.True(pair1.IsReadOnly); + + // Change one value and verify the other is unchanged. + pair0.Value = "othervalue"; + Assert.Equal("othervalue", pair0.Value); + Assert.Equal("value", pair1.Value); + Assert.Throws<InvalidOperationException>(() => { pair1.Value = "othervalue"; }); + } + + [Fact] + public void CopyFromReadOnly_NameAndValue_CopiedAsNonReadOnly() + { + var pair0 = new NameValueHeaderValue("name", "value"); + var pair1 = pair0.CopyAsReadOnly(); + var pair2 = pair1.Copy(); + Assert.NotSame(pair0, pair1); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); + + // Change one value and verify the other is unchanged. + pair2.Value = "othervalue"; + Assert.Equal("othervalue", pair2.Value); + Assert.Equal("value", pair1.Value); + } + + [Fact] + public void Value_CallSetterWithInvalidValues_Throw() + { + // Just verify that the setter calls the same validation the ctor invokes. + Assert.Throws<FormatException>(() => { var x = new NameValueHeaderValue("name"); x.Value = " x "; }); + Assert.Throws<FormatException>(() => { var x = new NameValueHeaderValue("name"); x.Value = "x y"; }); + } + + [Fact] + public void ToString_UseNoValueAndTokenAndQuotedStringValues_SerializedCorrectly() + { + var nameValue = new NameValueHeaderValue("text", "token"); + Assert.Equal("text=token", nameValue.ToString()); + + nameValue.Value = "\"quoted string\""; + Assert.Equal("text=\"quoted string\"", nameValue.ToString()); + + nameValue.Value = null; + Assert.Equal("text", nameValue.ToString()); + + nameValue.Value = string.Empty; + Assert.Equal("text", nameValue.ToString()); + } + + [Fact] + public void GetHashCode_ValuesUseDifferentValues_HashDiffersAccordingToRfc() + { + var nameValue1 = new NameValueHeaderValue("text"); + var nameValue2 = new NameValueHeaderValue("text"); + + nameValue1.Value = null; + nameValue2.Value = null; + Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + + nameValue1.Value = "token"; + nameValue2.Value = null; + Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + + nameValue1.Value = "token"; + nameValue2.Value = string.Empty; + Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + + nameValue1.Value = null; + nameValue2.Value = string.Empty; + Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + + nameValue1.Value = "token"; + nameValue2.Value = "TOKEN"; + Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + + nameValue1.Value = "token"; + nameValue2.Value = "token"; + Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + + nameValue1.Value = "\"quoted string\""; + nameValue2.Value = "\"QUOTED STRING\""; + Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + + nameValue1.Value = "\"quoted string\""; + nameValue2.Value = "\"quoted string\""; + Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + } + + [Fact] + public void GetHashCode_NameUseDifferentCasing_HashDiffersAccordingToRfc() + { + var nameValue1 = new NameValueHeaderValue("text"); + var nameValue2 = new NameValueHeaderValue("TEXT"); + Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + } + + [Fact] + public void Equals_ValuesUseDifferentValues_ValuesAreEqualOrDifferentAccordingToRfc() + { + var nameValue1 = new NameValueHeaderValue("text"); + var nameValue2 = new NameValueHeaderValue("text"); + + nameValue1.Value = null; + nameValue2.Value = null; + Assert.True(nameValue1.Equals(nameValue2), "<null> vs. <null>."); + + nameValue1.Value = "token"; + nameValue2.Value = null; + Assert.False(nameValue1.Equals(nameValue2), "token vs. <null>."); + + nameValue1.Value = null; + nameValue2.Value = "token"; + Assert.False(nameValue1.Equals(nameValue2), "<null> vs. token."); + + nameValue1.Value = string.Empty; + nameValue2.Value = "token"; + Assert.False(nameValue1.Equals(nameValue2), "string.Empty vs. token."); + + nameValue1.Value = null; + nameValue2.Value = string.Empty; + Assert.True(nameValue1.Equals(nameValue2), "<null> vs. string.Empty."); + + nameValue1.Value = "token"; + nameValue2.Value = "TOKEN"; + Assert.True(nameValue1.Equals(nameValue2), "token vs. TOKEN."); + + nameValue1.Value = "token"; + nameValue2.Value = "token"; + Assert.True(nameValue1.Equals(nameValue2), "token vs. token."); + + nameValue1.Value = "\"quoted string\""; + nameValue2.Value = "\"QUOTED STRING\""; + Assert.False(nameValue1.Equals(nameValue2), "\"quoted string\" vs. \"QUOTED STRING\"."); + + nameValue1.Value = "\"quoted string\""; + nameValue2.Value = "\"quoted string\""; + Assert.True(nameValue1.Equals(nameValue2), "\"quoted string\" vs. \"quoted string\"."); + + Assert.False(nameValue1.Equals(null), "\"quoted string\" vs. <null>."); + } + + [Fact] + public void Equals_NameUseDifferentCasing_ConsideredEqual() + { + var nameValue1 = new NameValueHeaderValue("text"); + var nameValue2 = new NameValueHeaderValue("TEXT"); + Assert.True(nameValue1.Equals(nameValue2), "text vs. TEXT."); + } + + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidParse(" name = value ", new NameValueHeaderValue("name", "value")); + CheckValidParse(" name", new NameValueHeaderValue("name")); + CheckValidParse(" name ", new NameValueHeaderValue("name")); + CheckValidParse(" name=\"value\"", new NameValueHeaderValue("name", "\"value\"")); + CheckValidParse("name=value", new NameValueHeaderValue("name", "value")); + CheckValidParse("name=\"quoted str\"", new NameValueHeaderValue("name", "\"quoted str\"")); + CheckValidParse("name\t =va1ue", new NameValueHeaderValue("name", "va1ue")); + CheckValidParse("name= va*ue ", new NameValueHeaderValue("name", "va*ue")); + CheckValidParse("name=", new NameValueHeaderValue("name", "")); + } + + [Fact] + public void Parse_SetOfInvalidValueStrings_Throws() + { + CheckInvalidParse("name[value"); + CheckInvalidParse("name=value="); + CheckInvalidParse("name=会"); + CheckInvalidParse("name==value"); + CheckInvalidParse("name= va:ue"); + CheckInvalidParse("=value"); + CheckInvalidParse("name value"); + CheckInvalidParse("name=,value"); + CheckInvalidParse("会"); + CheckInvalidParse(null); + CheckInvalidParse(string.Empty); + CheckInvalidParse(" "); + CheckInvalidParse(" ,,"); + CheckInvalidParse(" , , name = value , "); + CheckInvalidParse(" name,"); + CheckInvalidParse(" ,name=\"value\""); + } + + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidTryParse(" name = value ", new NameValueHeaderValue("name", "value")); + CheckValidTryParse(" name", new NameValueHeaderValue("name")); + CheckValidTryParse(" name=\"value\"", new NameValueHeaderValue("name", "\"value\"")); + CheckValidTryParse("name=value", new NameValueHeaderValue("name", "value")); + } + + [Fact] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() + { + CheckInvalidTryParse("name[value"); + CheckInvalidTryParse("name=value="); + CheckInvalidTryParse("name=会"); + CheckInvalidTryParse("name==value"); + CheckInvalidTryParse("=value"); + CheckInvalidTryParse("name value"); + CheckInvalidTryParse("name=,value"); + CheckInvalidTryParse("会"); + CheckInvalidTryParse(null); + CheckInvalidTryParse(string.Empty); + CheckInvalidTryParse(" "); + CheckInvalidTryParse(" ,,"); + CheckInvalidTryParse(" , , name = value , "); + CheckInvalidTryParse(" name,"); + CheckInvalidTryParse(" ,name=\"value\""); + } + + [Fact] + public void ParseList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] + { + "", + "name=value1", + "", + " name = value2 ", + "\r\n name =value3\r\n ", + "name=\"value 4\"", + "name=\"value会5\"", + "name=value6,name=value7", + "name=\"value 8\", name= \"value 9\"", + }; + var results = NameValueHeaderValue.ParseList(inputs); + + var expectedResults = new[] + { + new NameValueHeaderValue("name", "value1"), + new NameValueHeaderValue("name", "value2"), + new NameValueHeaderValue("name", "value3"), + new NameValueHeaderValue("name", "\"value 4\""), + new NameValueHeaderValue("name", "\"value会5\""), + new NameValueHeaderValue("name", "value6"), + new NameValueHeaderValue("name", "value7"), + new NameValueHeaderValue("name", "\"value 8\""), + new NameValueHeaderValue("name", "\"value 9\""), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void ParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] + { + "", + "name=value1", + "", + " name = value2 ", + "\r\n name =value3\r\n ", + "name=\"value 4\"", + "name=\"value会5\"", + "name=value6,name=value7", + "name=\"value 8\", name= \"value 9\"", + }; + var results = NameValueHeaderValue.ParseStrictList(inputs); + + var expectedResults = new[] + { + new NameValueHeaderValue("name", "value1"), + new NameValueHeaderValue("name", "value2"), + new NameValueHeaderValue("name", "value3"), + new NameValueHeaderValue("name", "\"value 4\""), + new NameValueHeaderValue("name", "\"value会5\""), + new NameValueHeaderValue("name", "value6"), + new NameValueHeaderValue("name", "value7"), + new NameValueHeaderValue("name", "\"value 8\""), + new NameValueHeaderValue("name", "\"value 9\""), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void TryParseList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] + { + "", + "name=value1", + "", + " name = value2 ", + "\r\n name =value3\r\n ", + "name=\"value 4\"", + "name=\"value会5\"", + "name=value6,name=value7", + "name=\"value 8\", name= \"value 9\"", + }; + IList<NameValueHeaderValue> results; + Assert.True(NameValueHeaderValue.TryParseList(inputs, out results)); + + var expectedResults = new[] + { + new NameValueHeaderValue("name", "value1"), + new NameValueHeaderValue("name", "value2"), + new NameValueHeaderValue("name", "value3"), + new NameValueHeaderValue("name", "\"value 4\""), + new NameValueHeaderValue("name", "\"value会5\""), + new NameValueHeaderValue("name", "value6"), + new NameValueHeaderValue("name", "value7"), + new NameValueHeaderValue("name", "\"value 8\""), + new NameValueHeaderValue("name", "\"value 9\""), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void TryParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] + { + "", + "name=value1", + "", + " name = value2 ", + "\r\n name =value3\r\n ", + "name=\"value 4\"", + "name=\"value会5\"", + "name=value6,name=value7", + "name=\"value 8\", name= \"value 9\"", + }; + IList<NameValueHeaderValue> results; + Assert.True(NameValueHeaderValue.TryParseStrictList(inputs, out results)); + + var expectedResults = new[] + { + new NameValueHeaderValue("name", "value1"), + new NameValueHeaderValue("name", "value2"), + new NameValueHeaderValue("name", "value3"), + new NameValueHeaderValue("name", "\"value 4\""), + new NameValueHeaderValue("name", "\"value会5\""), + new NameValueHeaderValue("name", "value6"), + new NameValueHeaderValue("name", "value7"), + new NameValueHeaderValue("name", "\"value 8\""), + new NameValueHeaderValue("name", "\"value 9\""), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void ParseList_WithSomeInvlaidValues_ExcludesInvalidValues() + { + var inputs = new[] + { + "", + "name1=value1", + "name2", + " name3 = 3, value a", + "name4 =value4, name5 = value5 b", + "name6=\"value 6", + "name7=\"value会7\"", + "name8=value8,name9=value9", + "name10=\"value 10\", name11= \"value 11\"", + }; + var results = NameValueHeaderValue.ParseList(inputs); + + var expectedResults = new[] + { + new NameValueHeaderValue("name1", "value1"), + new NameValueHeaderValue("name2"), + new NameValueHeaderValue("name3", "3"), + new NameValueHeaderValue("a"), + new NameValueHeaderValue("name4", "value4"), + new NameValueHeaderValue("b"), + new NameValueHeaderValue("6"), + new NameValueHeaderValue("name7", "\"value会7\""), + new NameValueHeaderValue("name8", "value8"), + new NameValueHeaderValue("name9", "value9"), + new NameValueHeaderValue("name10", "\"value 10\""), + new NameValueHeaderValue("name11", "\"value 11\""), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void ParseStrictList_WithSomeInvlaidValues_Throws() + { + var inputs = new[] + { + "", + "name1=value1", + "name2", + " name3 = 3, value a", + "name4 =value4, name5 = value5 b", + "name6=\"value 6", + "name7=\"value会7\"", + "name8=value8,name9=value9", + "name10=\"value 10\", name11= \"value 11\"", + }; + Assert.Throws<FormatException>(() => NameValueHeaderValue.ParseStrictList(inputs)); + } + + [Fact] + public void TryParseList_WithSomeInvlaidValues_ExcludesInvalidValues() + { + var inputs = new[] + { + "", + "name1=value1", + "name2", + " name3 = 3, value a", + "name4 =value4, name5 = value5 b", + "name6=\"value 6", + "name7=\"value会7\"", + "name8=value8,name9=value9", + "name10=\"value 10\", name11= \"value 11\"", + }; + IList<NameValueHeaderValue> results; + Assert.True(NameValueHeaderValue.TryParseList(inputs, out results)); + + var expectedResults = new[] + { + new NameValueHeaderValue("name1", "value1"), + new NameValueHeaderValue("name2"), + new NameValueHeaderValue("name3", "3"), + new NameValueHeaderValue("a"), + new NameValueHeaderValue("name4", "value4"), + new NameValueHeaderValue("b"), + new NameValueHeaderValue("6"), + new NameValueHeaderValue("name7", "\"value会7\""), + new NameValueHeaderValue("name8", "value8"), + new NameValueHeaderValue("name9", "value9"), + new NameValueHeaderValue("name10", "\"value 10\""), + new NameValueHeaderValue("name11", "\"value 11\""), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void TryParseStrictList_WithSomeInvlaidValues_ReturnsFalse() + { + var inputs = new[] + { + "", + "name1=value1", + "name2", + " name3 = 3, value a", + "name4 =value4, name5 = value5 b", + "name6=\"value 6", + "name7=\"value会7\"", + "name8=value8,name9=value9", + "name10=\"value 10\", name11= \"value 11\"", + }; + IList<NameValueHeaderValue> results; + Assert.False(NameValueHeaderValue.TryParseStrictList(inputs, out results)); + } + + [Theory] + [InlineData("value", "value")] + [InlineData("\"value\"", "value")] + [InlineData("\"hello\\\\\"", "hello\\")] + [InlineData("\"hello\\\"\"", "hello\"")] + [InlineData("\"hello\\\"foo\\\\bar\\\\baz\\\\\"", "hello\"foo\\bar\\baz\\")] + [InlineData("\"quoted value\"", "quoted value")] + [InlineData("\"quoted\\\"valuewithquote\"", "quoted\"valuewithquote")] + [InlineData("\"hello\\\"", "hello\\")] + public void GetUnescapedValue_ReturnsExpectedValue(string input, string expected) + { + var header = new NameValueHeaderValue("test", input); + + var actual = header.GetUnescapedValue(); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("value", "value")] + [InlineData("23", "23")] + [InlineData(";;;", "\";;;\"")] + [InlineData("\"value\"", "\"value\"")] + [InlineData("\"assumes already encoded \\\"\"", "\"assumes already encoded \\\"\"")] + [InlineData("unquoted \"value", "\"unquoted \\\"value\"")] + [InlineData("value\\morevalues\\evenmorevalues", "\"value\\\\morevalues\\\\evenmorevalues\"")] + // We have to assume that the input needs to be quoted here + [InlineData("\"\"double quoted string\"\"", "\"\\\"\\\"double quoted string\\\"\\\"\"")] + [InlineData("\t", "\"\t\"")] + public void SetAndEscapeValue_ReturnsExpectedValue(string input, string expected) + { + var header = new NameValueHeaderValue("test"); + header.SetAndEscapeValue(input); + + var actual = header.Value; + + Assert.Equal(expected, actual); + } + + + [Theory] + [InlineData("\n")] + [InlineData("\b")] + [InlineData("\r")] + public void SetAndEscapeValue_ThrowsOnInvalidValues(string input) + { + var header = new NameValueHeaderValue("test"); + Assert.Throws<FormatException>(() => header.SetAndEscapeValue(input)); + } + + [Theory] + [InlineData("value")] + [InlineData("\"value\\\\morevalues\\\\evenmorevalues\"")] + [InlineData("\"quoted \\\"value\"")] + public void GetAndSetEncodeValueRoundTrip_ReturnsExpectedValue(string input) + { + var header = new NameValueHeaderValue("test"); + header.Value = input; + var valueHeader = header.GetUnescapedValue(); + header.SetAndEscapeValue(valueHeader); + + var actual = header.Value; + + Assert.Equal(input, actual); + } + + [Theory] + [InlineData("val\\nue")] + [InlineData("val\\bue")] + public void OverescapingValuesDoNotRoundTrip(string input) + { + var header = new NameValueHeaderValue("test"); + header.SetAndEscapeValue(input); + var valueHeader = header.GetUnescapedValue(); + + var actual = header.Value; + + Assert.NotEqual(input, actual); + } + + + #region Helper methods + + private void CheckValidParse(string input, NameValueHeaderValue expectedResult) + { + var result = NameValueHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidParse(string input) + { + Assert.Throws<FormatException>(() => NameValueHeaderValue.Parse(input)); + } + + private void CheckValidTryParse(string input, NameValueHeaderValue expectedResult) + { + NameValueHeaderValue result = null; + Assert.True(NameValueHeaderValue.TryParse(input, out result)); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidTryParse(string input) + { + NameValueHeaderValue result = null; + Assert.False(NameValueHeaderValue.TryParse(input, out result)); + Assert.Null(result); + } + + private static void CheckValue(string value) + { + var nameValue = new NameValueHeaderValue("text", value); + Assert.Equal(value, nameValue.Value); + } + + private static void AssertFormatException(string name, string value) + { + Assert.Throws<FormatException>(() => new NameValueHeaderValue(name, value)); + } + + #endregion + } +} diff --git a/src/Http/Headers/test/RangeConditionHeaderValueTest.cs b/src/Http/Headers/test/RangeConditionHeaderValueTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..ce7c73997b8d3e09a9611549a2305898286c68c1 --- /dev/null +++ b/src/Http/Headers/test/RangeConditionHeaderValueTest.cs @@ -0,0 +1,174 @@ +// 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 Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class RangeConditionHeaderValueTest + { + [Fact] + public void Ctor_EntityTagOverload_MatchExpectation() + { + var rangeCondition = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\"")); + Assert.Equal(new EntityTagHeaderValue("\"x\""), rangeCondition.EntityTag); + Assert.Null(rangeCondition.LastModified); + + EntityTagHeaderValue input = null; + Assert.Throws<ArgumentNullException>(() => new RangeConditionHeaderValue(input)); + } + + [Fact] + public void Ctor_EntityTagStringOverload_MatchExpectation() + { + var rangeCondition = new RangeConditionHeaderValue("\"y\""); + Assert.Equal(new EntityTagHeaderValue("\"y\""), rangeCondition.EntityTag); + Assert.Null(rangeCondition.LastModified); + + Assert.Throws<ArgumentException>(() => new RangeConditionHeaderValue((string)null)); + } + + [Fact] + public void Ctor_DateOverload_MatchExpectation() + { + var rangeCondition = new RangeConditionHeaderValue( + new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); + Assert.Null(rangeCondition.EntityTag); + Assert.Equal(new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero), rangeCondition.LastModified); + } + + [Fact] + public void ToString_UseDifferentrangeConditions_AllSerializedCorrectly() + { + var rangeCondition = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\"")); + Assert.Equal("\"x\"", rangeCondition.ToString()); + + rangeCondition = new RangeConditionHeaderValue(new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); + Assert.Equal("Thu, 15 Jul 2010 12:33:57 GMT", rangeCondition.ToString()); + } + + [Fact] + public void GetHashCode_UseSameAndDifferentrangeConditions_SameOrDifferentHashCodes() + { + var rangeCondition1 = new RangeConditionHeaderValue("\"x\""); + var rangeCondition2 = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\"")); + var rangeCondition3 = new RangeConditionHeaderValue( + new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); + var rangeCondition4 = new RangeConditionHeaderValue( + new DateTimeOffset(2008, 8, 16, 13, 44, 10, TimeSpan.Zero)); + var rangeCondition5 = new RangeConditionHeaderValue( + new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); + var rangeCondition6 = new RangeConditionHeaderValue( + new EntityTagHeaderValue("\"x\"", true)); + + Assert.Equal(rangeCondition1.GetHashCode(), rangeCondition2.GetHashCode()); + Assert.NotEqual(rangeCondition1.GetHashCode(), rangeCondition3.GetHashCode()); + Assert.NotEqual(rangeCondition3.GetHashCode(), rangeCondition4.GetHashCode()); + Assert.Equal(rangeCondition3.GetHashCode(), rangeCondition5.GetHashCode()); + Assert.NotEqual(rangeCondition1.GetHashCode(), rangeCondition6.GetHashCode()); + } + + [Fact] + public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() + { + var rangeCondition1 = new RangeConditionHeaderValue("\"x\""); + var rangeCondition2 = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\"")); + var rangeCondition3 = new RangeConditionHeaderValue( + new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); + var rangeCondition4 = new RangeConditionHeaderValue( + new DateTimeOffset(2008, 8, 16, 13, 44, 10, TimeSpan.Zero)); + var rangeCondition5 = new RangeConditionHeaderValue( + new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); + var rangeCondition6 = new RangeConditionHeaderValue( + new EntityTagHeaderValue("\"x\"", true)); + + Assert.False(rangeCondition1.Equals(null), "\"x\" vs. <null>"); + Assert.True(rangeCondition1.Equals(rangeCondition2), "\"x\" vs. \"x\""); + Assert.False(rangeCondition1.Equals(rangeCondition3), "\"x\" vs. date"); + Assert.False(rangeCondition3.Equals(rangeCondition1), "date vs. \"x\""); + Assert.False(rangeCondition3.Equals(rangeCondition4), "date vs. different date"); + Assert.True(rangeCondition3.Equals(rangeCondition5), "date vs. date"); + Assert.False(rangeCondition1.Equals(rangeCondition6), "\"x\" vs. W/\"x\""); + } + + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidParse(" \"x\" ", new RangeConditionHeaderValue("\"x\"")); + CheckValidParse(" Sun, 06 Nov 1994 08:49:37 GMT ", + new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero))); + CheckValidParse("Wed, 09 Nov 1994 08:49:37 GMT", + new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 9, 8, 49, 37, TimeSpan.Zero))); + CheckValidParse(" W/ \"tag\" ", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true))); + CheckValidParse(" w/\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true))); + CheckValidParse("\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\""))); + } + + [Theory] + [InlineData("\"x\" ,")] // no delimiter allowed + [InlineData("Sun, 06 Nov 1994 08:49:37 GMT ,")] // no delimiter allowed + [InlineData("\"x\" Sun, 06 Nov 1994 08:49:37 GMT")] + [InlineData("Sun, 06 Nov 1994 08:49:37 GMT \"x\"")] + [InlineData(null)] + [InlineData("")] + [InlineData(" Wed 09 Nov 1994 08:49:37 GMT")] + [InlineData("\"x")] + [InlineData("Wed, 09 Nov")] + [InlineData("W/Wed 09 Nov 1994 08:49:37 GMT")] + [InlineData("\"x\",")] + [InlineData("Wed 09 Nov 1994 08:49:37 GMT,")] + public void Parse_SetOfInvalidValueStrings_Throws(string input) + { + Assert.Throws<FormatException>(() => RangeConditionHeaderValue.Parse(input)); + } + + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidTryParse(" \"x\" ", new RangeConditionHeaderValue("\"x\"")); + CheckValidTryParse(" Sun, 06 Nov 1994 08:49:37 GMT ", + new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero))); + CheckValidTryParse(" W/ \"tag\" ", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true))); + CheckValidTryParse(" w/\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true))); + CheckValidTryParse("\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\""))); + } + + [Theory] + [InlineData("\"x\" ,")] // no delimiter allowed + [InlineData("Sun, 06 Nov 1994 08:49:37 GMT ,")] // no delimiter allowed + [InlineData("\"x\" Sun, 06 Nov 1994 08:49:37 GMT")] + [InlineData("Sun, 06 Nov 1994 08:49:37 GMT \"x\"")] + [InlineData(null)] + [InlineData("")] + [InlineData(" Wed 09 Nov 1994 08:49:37 GMT")] + [InlineData("\"x")] + [InlineData("Wed, 09 Nov")] + [InlineData("W/Wed 09 Nov 1994 08:49:37 GMT")] + [InlineData("\"x\",")] + [InlineData("Wed 09 Nov 1994 08:49:37 GMT,")] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse(string input) + { + RangeConditionHeaderValue result = null; + Assert.False(RangeConditionHeaderValue.TryParse(input, out result)); + Assert.Null(result); + } + + #region Helper methods + + private void CheckValidParse(string input, RangeConditionHeaderValue expectedResult) + { + var result = RangeConditionHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } + + private void CheckValidTryParse(string input, RangeConditionHeaderValue expectedResult) + { + RangeConditionHeaderValue result = null; + Assert.True(RangeConditionHeaderValue.TryParse(input, out result)); + Assert.Equal(expectedResult, result); + } + + #endregion + } +} diff --git a/src/Http/Headers/test/RangeHeaderValueTest.cs b/src/Http/Headers/test/RangeHeaderValueTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..92a1d72521fcbd57f45e7773b507369047fa70f2 --- /dev/null +++ b/src/Http/Headers/test/RangeHeaderValueTest.cs @@ -0,0 +1,183 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class RangeHeaderValueTest + { + [Fact] + public void Ctor_InvalidRange_Throw() + { + Assert.Throws<ArgumentOutOfRangeException>(() => new RangeHeaderValue(5, 2)); + } + + [Fact] + public void Unit_GetAndSetValidAndInvalidValues_MatchExpectation() + { + var range = new RangeHeaderValue(); + range.Unit = "myunit"; + Assert.Equal("myunit", range.Unit); + + Assert.Throws<ArgumentException>(() => range.Unit = null); + Assert.Throws<ArgumentException>(() => range.Unit = ""); + Assert.Throws<FormatException>(() => range.Unit = " x"); + Assert.Throws<FormatException>(() => range.Unit = "x "); + Assert.Throws<FormatException>(() => range.Unit = "x y"); + } + + [Fact] + public void ToString_UseDifferentRanges_AllSerializedCorrectly() + { + var range = new RangeHeaderValue(); + range.Unit = "myunit"; + range.Ranges.Add(new RangeItemHeaderValue(1, 3)); + Assert.Equal("myunit=1-3", range.ToString()); + + range.Ranges.Add(new RangeItemHeaderValue(5, null)); + range.Ranges.Add(new RangeItemHeaderValue(null, 17)); + Assert.Equal("myunit=1-3, 5-, -17", range.ToString()); + } + + [Fact] + public void GetHashCode_UseSameAndDifferentRanges_SameOrDifferentHashCodes() + { + var range1 = new RangeHeaderValue(1, 2); + var range2 = new RangeHeaderValue(1, 2); + range2.Unit = "BYTES"; + var range3 = new RangeHeaderValue(1, null); + var range4 = new RangeHeaderValue(null, 2); + var range5 = new RangeHeaderValue(); + range5.Ranges.Add(new RangeItemHeaderValue(1, 2)); + range5.Ranges.Add(new RangeItemHeaderValue(3, 4)); + var range6 = new RangeHeaderValue(); + range6.Ranges.Add(new RangeItemHeaderValue(3, 4)); // reverse order of range5 + range6.Ranges.Add(new RangeItemHeaderValue(1, 2)); + + Assert.Equal(range1.GetHashCode(), range2.GetHashCode()); + Assert.NotEqual(range1.GetHashCode(), range3.GetHashCode()); + Assert.NotEqual(range1.GetHashCode(), range4.GetHashCode()); + Assert.NotEqual(range1.GetHashCode(), range5.GetHashCode()); + Assert.Equal(range5.GetHashCode(), range6.GetHashCode()); + } + + [Fact] + public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() + { + var range1 = new RangeHeaderValue(1, 2); + var range2 = new RangeHeaderValue(1, 2); + range2.Unit = "BYTES"; + var range3 = new RangeHeaderValue(1, null); + var range4 = new RangeHeaderValue(null, 2); + var range5 = new RangeHeaderValue(); + range5.Ranges.Add(new RangeItemHeaderValue(1, 2)); + range5.Ranges.Add(new RangeItemHeaderValue(3, 4)); + var range6 = new RangeHeaderValue(); + range6.Ranges.Add(new RangeItemHeaderValue(3, 4)); // reverse order of range5 + range6.Ranges.Add(new RangeItemHeaderValue(1, 2)); + var range7 = new RangeHeaderValue(1, 2); + range7.Unit = "other"; + + Assert.False(range1.Equals(null), "bytes=1-2 vs. <null>"); + Assert.True(range1.Equals(range2), "bytes=1-2 vs. BYTES=1-2"); + Assert.False(range1.Equals(range3), "bytes=1-2 vs. bytes=1-"); + Assert.False(range1.Equals(range4), "bytes=1-2 vs. bytes=-2"); + Assert.False(range1.Equals(range5), "bytes=1-2 vs. bytes=1-2,3-4"); + Assert.True(range5.Equals(range6), "bytes=1-2,3-4 vs. bytes=3-4,1-2"); + Assert.False(range1.Equals(range7), "bytes=1-2 vs. other=1-2"); + } + + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidParse(" bytes=1-2 ", new RangeHeaderValue(1, 2)); + + var expected = new RangeHeaderValue(); + expected.Unit = "custom"; + expected.Ranges.Add(new RangeItemHeaderValue(null, 5)); + expected.Ranges.Add(new RangeItemHeaderValue(1, 4)); + CheckValidParse("custom = - 5 , 1 - 4 ,,", expected); + + expected = new RangeHeaderValue(); + expected.Unit = "custom"; + expected.Ranges.Add(new RangeItemHeaderValue(1, 2)); + CheckValidParse(" custom = 1 - 2", expected); + + expected = new RangeHeaderValue(); + expected.Ranges.Add(new RangeItemHeaderValue(1, 2)); + expected.Ranges.Add(new RangeItemHeaderValue(3, null)); + expected.Ranges.Add(new RangeItemHeaderValue(null, 4)); + CheckValidParse("bytes =1-2,,3-, , ,-4,,", expected); + } + + [Fact] + public void Parse_SetOfInvalidValueStrings_Throws() + { + CheckInvalidParse("bytes=1-2x"); // only delimiter ',' allowed after last range + CheckInvalidParse("x bytes=1-2"); + CheckInvalidParse("bytes=1-2.4"); + CheckInvalidParse(null); + CheckInvalidParse(string.Empty); + + CheckInvalidParse("bytes=1"); + CheckInvalidParse("bytes="); + CheckInvalidParse("bytes"); + CheckInvalidParse("bytes 1-2"); + CheckInvalidParse("bytes= ,,, , ,,"); + } + + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidTryParse(" bytes=1-2 ", new RangeHeaderValue(1, 2)); + + var expected = new RangeHeaderValue(); + expected.Unit = "custom"; + expected.Ranges.Add(new RangeItemHeaderValue(null, 5)); + expected.Ranges.Add(new RangeItemHeaderValue(1, 4)); + CheckValidTryParse("custom = - 5 , 1 - 4 ,,", expected); + } + + [Fact] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() + { + CheckInvalidTryParse("bytes=1-2x"); // only delimiter ',' allowed after last range + CheckInvalidTryParse("x bytes=1-2"); + CheckInvalidTryParse("bytes=1-2.4"); + CheckInvalidTryParse(null); + CheckInvalidTryParse(string.Empty); + } + + #region Helper methods + + private void CheckValidParse(string input, RangeHeaderValue expectedResult) + { + var result = RangeHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidParse(string input) + { + Assert.Throws<FormatException>(() => RangeHeaderValue.Parse(input)); + } + + private void CheckValidTryParse(string input, RangeHeaderValue expectedResult) + { + RangeHeaderValue result = null; + Assert.True(RangeHeaderValue.TryParse(input, out result)); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidTryParse(string input) + { + RangeHeaderValue result = null; + Assert.False(RangeHeaderValue.TryParse(input, out result)); + Assert.Null(result); + } + + #endregion + } +} diff --git a/src/Http/Headers/test/RangeItemHeaderValueTest.cs b/src/Http/Headers/test/RangeItemHeaderValueTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..95598f0a46f4bc83c221a5fdf91afecbfc15c425 --- /dev/null +++ b/src/Http/Headers/test/RangeItemHeaderValueTest.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; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class RangeItemHeaderValueTest + { + [Fact] + public void Ctor_BothValuesNull_Throw() + { + Assert.Throws<ArgumentException>(() => new RangeItemHeaderValue(null, null)); + } + + [Fact] + public void Ctor_FromValueNegative_Throw() + { + Assert.Throws<ArgumentOutOfRangeException>(() => new RangeItemHeaderValue(-1, null)); + } + + [Fact] + public void Ctor_FromGreaterThanToValue_Throw() + { + Assert.Throws<ArgumentOutOfRangeException>(() => new RangeItemHeaderValue(2, 1)); + } + + [Fact] + public void Ctor_ToValueNegative_Throw() + { + Assert.Throws<ArgumentOutOfRangeException>(() => new RangeItemHeaderValue(null, -1)); + } + + [Fact] + public void Ctor_ValidFormat_SuccessfullyCreated() + { + var rangeItem = new RangeItemHeaderValue(1, 2); + Assert.Equal(1, rangeItem.From); + Assert.Equal(2, rangeItem.To); + } + + [Fact] + public void ToString_UseDifferentRangeItems_AllSerializedCorrectly() + { + // Make sure ToString() doesn't add any separators. + var rangeItem = new RangeItemHeaderValue(1000000000, 2000000000); + Assert.Equal("1000000000-2000000000", rangeItem.ToString()); + + rangeItem = new RangeItemHeaderValue(5, null); + Assert.Equal("5-", rangeItem.ToString()); + + rangeItem = new RangeItemHeaderValue(null, 10); + Assert.Equal("-10", rangeItem.ToString()); + } + + [Fact] + public void GetHashCode_UseSameAndDifferentRangeItems_SameOrDifferentHashCodes() + { + var rangeItem1 = new RangeItemHeaderValue(1, 2); + var rangeItem2 = new RangeItemHeaderValue(1, null); + var rangeItem3 = new RangeItemHeaderValue(null, 2); + var rangeItem4 = new RangeItemHeaderValue(2, 2); + var rangeItem5 = new RangeItemHeaderValue(1, 2); + + Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem2.GetHashCode()); + Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem3.GetHashCode()); + Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem4.GetHashCode()); + Assert.Equal(rangeItem1.GetHashCode(), rangeItem5.GetHashCode()); + } + + [Fact] + public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() + { + var rangeItem1 = new RangeItemHeaderValue(1, 2); + var rangeItem2 = new RangeItemHeaderValue(1, null); + var rangeItem3 = new RangeItemHeaderValue(null, 2); + var rangeItem4 = new RangeItemHeaderValue(2, 2); + var rangeItem5 = new RangeItemHeaderValue(1, 2); + + Assert.False(rangeItem1.Equals(rangeItem2), "1-2 vs. 1-."); + Assert.False(rangeItem2.Equals(rangeItem1), "1- vs. 1-2."); + Assert.False(rangeItem1.Equals(null), "1-2 vs. null."); + Assert.False(rangeItem1.Equals(rangeItem3), "1-2 vs. -2."); + Assert.False(rangeItem3.Equals(rangeItem1), "-2 vs. 1-2."); + Assert.False(rangeItem1.Equals(rangeItem4), "1-2 vs. 2-2."); + Assert.True(rangeItem1.Equals(rangeItem5), "1-2 vs. 1-2."); + } + + [Fact] + public void TryParse_DifferentValidScenarios_AllReturnNonZero() + { + CheckValidTryParse("1-2", 1, 2); + CheckValidTryParse(" 1-2", 1, 2); + CheckValidTryParse("0-0", 0, 0); + CheckValidTryParse(" 1-", 1, null); + CheckValidTryParse(" -2", null, 2); + + CheckValidTryParse(" 684684 - 123456789012345 ", 684684, 123456789012345); + + // The separator doesn't matter. It only parses until the first non-whitespace + CheckValidTryParse(" 1 - 2 ,", 1, 2); + + CheckValidTryParse(",,1-2, 3 - , , -6 , ,,", new Tuple<long?, long?>(1, 2), new Tuple<long?, long?>(3, null), + new Tuple<long?, long?>(null, 6)); + CheckValidTryParse("1-2,", new Tuple<long?, long?>(1, 2)); + CheckValidTryParse("1-", new Tuple<long?, long?>(1, null)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(",,")] + [InlineData("1")] + [InlineData("1-2,3")] + [InlineData("1--2")] + [InlineData("1,-2")] + [InlineData("-")] + [InlineData("--")] + [InlineData("2-1")] + [InlineData("12345678901234567890123-")] // >>Int64.MaxValue + [InlineData("-12345678901234567890123")] // >>Int64.MaxValue + [InlineData("9999999999999999999-")] // 19-digit numbers outside the Int64 range. + [InlineData("-9999999999999999999")] // 19-digit numbers outside the Int64 range. + public void TryParse_DifferentInvalidScenarios_AllReturnFalse(string input) + { + RangeHeaderValue result; + Assert.False(RangeHeaderValue.TryParse("byte=" + input, out result)); + } + + private static void CheckValidTryParse(string input, long? expectedFrom, long? expectedTo) + { + RangeHeaderValue result; + Assert.True(RangeHeaderValue.TryParse("byte=" + input, out result), input); + + var ranges = result.Ranges.ToArray(); + Assert.Single(ranges); + + var range = ranges.First(); + + Assert.Equal(expectedFrom, range.From); + Assert.Equal(expectedTo, range.To); + } + + private static void CheckValidTryParse(string input, params Tuple<long?, long?>[] expectedRanges) + { + RangeHeaderValue result; + Assert.True(RangeHeaderValue.TryParse("byte=" + input, out result), input); + + var ranges = result.Ranges.ToArray(); + Assert.Equal(expectedRanges.Length, ranges.Length); + + for (int i = 0; i < expectedRanges.Length; i++) + { + Assert.Equal(expectedRanges[i].Item1, ranges[i].From); + Assert.Equal(expectedRanges[i].Item2, ranges[i].To); + } + } + } +} diff --git a/src/Http/Headers/test/SetCookieHeaderValueTest.cs b/src/Http/Headers/test/SetCookieHeaderValueTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..e7e8bf045a12c91fd982443669f5c5560d6bc4b3 --- /dev/null +++ b/src/Http/Headers/test/SetCookieHeaderValueTest.cs @@ -0,0 +1,429 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class SetCookieHeaderValueTest + { + public static TheoryData<SetCookieHeaderValue, string> SetCookieHeaderDataSet + { + get + { + var dataset = new TheoryData<SetCookieHeaderValue, string>(); + var header1 = new SetCookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3") + { + Domain = "domain1", + Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), + SameSite = SameSiteMode.Strict, + HttpOnly = true, + MaxAge = TimeSpan.FromDays(1), + Path = "path1", + Secure = true + }; + dataset.Add(header1, "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=strict; httponly"); + + var header2 = new SetCookieHeaderValue("name2", ""); + dataset.Add(header2, "name2="); + + var header3 = new SetCookieHeaderValue("name2", "value2"); + dataset.Add(header3, "name2=value2"); + + var header4 = new SetCookieHeaderValue("name4", "value4") + { + MaxAge = TimeSpan.FromDays(1), + }; + dataset.Add(header4, "name4=value4; max-age=86400"); + + var header5 = new SetCookieHeaderValue("name5", "value5") + { + Domain = "domain1", + Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), + }; + dataset.Add(header5, "name5=value5; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1"); + + var header6 = new SetCookieHeaderValue("name6", "value6") + { + SameSite = SameSiteMode.Lax, + }; + dataset.Add(header6, "name6=value6; samesite=lax"); + + var header7 = new SetCookieHeaderValue("name7", "value7") + { + SameSite = SameSiteMode.None, + }; + dataset.Add(header7, "name7=value7"); + + + return dataset; + } + } + + public static TheoryData<string> InvalidSetCookieHeaderDataSet + { + get + { + return new TheoryData<string> + { + "expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1", + "name=value; expires=Sun, 06 Nov 1994 08:49:37 ZZZ; max-age=86400; domain=domain1", + "name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=-86400; domain=domain1", + }; + } + } + + public static TheoryData<string> InvalidCookieNames + { + get + { + return new TheoryData<string> + { + "<acb>", + "{acb}", + "[acb]", + "\"acb\"", + "a,b", + "a;b", + "a\\b", + }; + } + } + + public static TheoryData<string> InvalidCookieValues + { + get + { + return new TheoryData<string> + { + { "\"" }, + { "a,b" }, + { "a;b" }, + { "a\\b" }, + { "\"abc" }, + { "a\"bc" }, + { "abc\"" }, + }; + } + } + + public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListOfSetCookieHeaderDataSet + { + get + { + var dataset = new TheoryData<IList<SetCookieHeaderValue>, string[]>(); + var header1 = new SetCookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3") + { + Domain = "domain1", + Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), + SameSite = SameSiteMode.Strict, + HttpOnly = true, + MaxAge = TimeSpan.FromDays(1), + Path = "path1", + Secure = true + }; + var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=strict; httponly"; + + var header2 = new SetCookieHeaderValue("name2", "value2"); + var string2 = "name2=value2"; + + var header3 = new SetCookieHeaderValue("name3", "value3") + { + MaxAge = TimeSpan.FromDays(1), + }; + var string3 = "name3=value3; max-age=86400"; + + var header4 = new SetCookieHeaderValue("name4", "value4") + { + Domain = "domain1", + Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), + }; + var string4 = "name4=value4; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1"; + + var header5 = new SetCookieHeaderValue("name5", "value5") + { + SameSite = SameSiteMode.Lax + }; + var string5a = "name5=value5; samesite=lax"; + var string5b = "name5=value5; samesite=Lax"; + + var header6 = new SetCookieHeaderValue("name6", "value6") + { + SameSite = SameSiteMode.Strict + }; + var string6a = "name6=value6; samesite"; + var string6b = "name6=value6; samesite=Strict"; + var string6c = "name6=value6; samesite=invalid"; + + dataset.Add(new[] { header1 }.ToList(), new[] { string1 }); + dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 }); + dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ",", " , ", string1 }); + dataset.Add(new[] { header2 }.ToList(), new[] { string2 }); + dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1, string2 }); + dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1 + ", " + string2 }); + dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + ", " + string1 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) }); + dataset.Add(new[] { header5 }.ToList(), new[] { string5a }); + dataset.Add(new[] { header5 }.ToList(), new[] { string5b }); + dataset.Add(new[] { header6 }.ToList(), new[] { string6a }); + dataset.Add(new[] { header6 }.ToList(), new[] { string6b }); + dataset.Add(new[] { header6 }.ToList(), new[] { string6c }); + + return dataset; + } + } + + public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListWithInvalidSetCookieHeaderDataSet + { + get + { + var dataset = new TheoryData<IList<SetCookieHeaderValue>, string[]>(); + var header1 = new SetCookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3") + { + Domain = "domain1", + Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), + SameSite = SameSiteMode.Strict, + HttpOnly = true, + MaxAge = TimeSpan.FromDays(1), + Path = "path1", + Secure = true + }; + var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=Strict; httponly"; + + var header2 = new SetCookieHeaderValue("name2", "value2"); + var string2 = "name2=value2"; + + var header3 = new SetCookieHeaderValue("name3", "value3") + { + MaxAge = TimeSpan.FromDays(1), + }; + var string3 = "name3=value3; max-age=86400"; + + var header4 = new SetCookieHeaderValue("name4", "value4") + { + Domain = "domain1", + Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), + }; + var string4 = "name4=value4; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1;"; + + var invalidString1 = "ipt={\"v\":{\"L\":3},\"pt:{\"d\":3},\"ct\":{},\"_t\":44,\"_v\":\"2\"}"; + + var invalidHeader2a = new SetCookieHeaderValue("expires", "Sun"); + var invalidHeader2b = new SetCookieHeaderValue("domain", "domain1"); + var invalidString2 = "ipt={\"v\":{\"L\":3},\"pt\":{d\":3},\"ct\":{},\"_t\":44,\"_v\":\"2\"}; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1"; + + var invalidHeader3 = new SetCookieHeaderValue("domain", "domain1") + { + Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), + }; + var invalidString3 = "ipt={\"v\":{\"L\":3},\"pt\":{\"d:3},\"ct\":{},\"_t\":44,\"_v\":\"2\"}; domain=domain1; expires=Sun, 06 Nov 1994 08:49:37 GMT"; + + dataset.Add(null, new[] { invalidString1 }); + dataset.Add(new[] { invalidHeader2a, invalidHeader2b }.ToList(), new[] { invalidString2 }); + dataset.Add(new[] { invalidHeader3 }.ToList(), new[] { invalidString3 }); + dataset.Add(new[] { header1 }.ToList(), new[] { string1, invalidString1 }); + dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1, null, "", " ", ",", " , ", string1 }); + dataset.Add(new[] { header1 }.ToList(), new[] { string1 + ", " + invalidString1 }); + dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1 + ", " + string1 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { invalidString1, string1, string2, string3, string4 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, invalidString1, string2, string3, string4 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, invalidString1, string3, string4 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, invalidString1, string4 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4, invalidString1 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", invalidString1, string1, string2, string3, string4) }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, invalidString1, string2, string3, string4) }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, invalidString1, string3, string4) }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, invalidString1, string4) }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4, invalidString1) }); + + return dataset; + } + } + + [Fact] + public void SetCookieHeaderValue_CtorThrowsOnNullName() + { + Assert.Throws<ArgumentNullException>(() => new SetCookieHeaderValue(null, "value")); + } + + [Theory] + [MemberData(nameof(InvalidCookieNames))] + public void SetCookieHeaderValue_CtorThrowsOnInvalidName(string name) + { + Assert.Throws<ArgumentException>(() => new SetCookieHeaderValue(name, "value")); + } + + [Theory] + [MemberData(nameof(InvalidCookieValues))] + public void SetCookieHeaderValue_CtorThrowsOnInvalidValue(string value) + { + Assert.Throws<ArgumentException>(() => new SetCookieHeaderValue("name", value)); + } + + [Fact] + public void SetCookieHeaderValue_Ctor1_InitializesCorrectly() + { + var header = new SetCookieHeaderValue("cookie"); + Assert.Equal("cookie", header.Name); + Assert.Equal(string.Empty, header.Value); + } + + [Theory] + [InlineData("name", "")] + [InlineData("name", "value")] + [InlineData("name", "\"acb\"")] + public void SetCookieHeaderValue_Ctor2InitializesCorrectly(string name, string value) + { + var header = new SetCookieHeaderValue(name, value); + Assert.Equal(name, header.Name); + Assert.Equal(value, header.Value); + } + + [Fact] + public void SetCookieHeaderValue_Value() + { + var cookie = new SetCookieHeaderValue("name"); + Assert.Equal(string.Empty, cookie.Value); + + cookie.Value = "value1"; + Assert.Equal("value1", cookie.Value); + } + + [Theory] + [MemberData(nameof(SetCookieHeaderDataSet))] + public void SetCookieHeaderValue_ToString(SetCookieHeaderValue input, string expectedValue) + { + Assert.Equal(expectedValue, input.ToString()); + } + + [Theory] + [MemberData(nameof(SetCookieHeaderDataSet))] + public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue) + { + var builder = new StringBuilder(); + + input.AppendToStringBuilder(builder); + + Assert.Equal(expectedValue, builder.ToString()); + } + + [Theory] + [MemberData(nameof(SetCookieHeaderDataSet))] + public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue) + { + var header = SetCookieHeaderValue.Parse(expectedValue); + + Assert.Equal(cookie, header); + Assert.Equal(expectedValue, header.ToString()); + } + + [Theory] + [MemberData(nameof(SetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue) + { + Assert.True(SetCookieHeaderValue.TryParse(expectedValue, out var header)); + + Assert.Equal(cookie, header); + Assert.Equal(expectedValue, header.ToString()); + } + + [Theory] + [MemberData(nameof(InvalidSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_Parse_RejectsInvalidValues(string value) + { + Assert.Throws<FormatException>(() => SetCookieHeaderValue.Parse(value)); + } + + [Theory] + [MemberData(nameof(InvalidSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParse_RejectsInvalidValues(string value) + { + Assert.False(SetCookieHeaderValue.TryParse(value, out var _)); + } + + [Theory] + [MemberData(nameof(ListOfSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_ParseList_AcceptsValidValues(IList<SetCookieHeaderValue> cookies, string[] input) + { + var results = SetCookieHeaderValue.ParseList(input); + + Assert.Equal(cookies, results); + } + + [Theory] + [MemberData(nameof(ListOfSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParseList_AcceptsValidValues(IList<SetCookieHeaderValue> cookies, string[] input) + { + bool result = SetCookieHeaderValue.TryParseList(input, out var results); + Assert.True(result); + + Assert.Equal(cookies, results); + } + + [Theory] + [MemberData(nameof(ListOfSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_ParseStrictList_AcceptsValidValues(IList<SetCookieHeaderValue> cookies, string[] input) + { + var results = SetCookieHeaderValue.ParseStrictList(input); + + Assert.Equal(cookies, results); + } + + [Theory] + [MemberData(nameof(ListOfSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParseStrictList_AcceptsValidValues(IList<SetCookieHeaderValue> cookies, string[] input) + { + bool result = SetCookieHeaderValue.TryParseStrictList(input, out var results); + Assert.True(result); + + Assert.Equal(cookies, results); + } + + [Theory] + [MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_ParseList_ExcludesInvalidValues(IList<SetCookieHeaderValue> cookies, string[] input) + { + var results = SetCookieHeaderValue.ParseList(input); + // ParseList aways returns a list, even if empty. TryParseList may return null (via out). + Assert.Equal(cookies ?? new List<SetCookieHeaderValue>(), results); + } + + [Theory] + [MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParseList_ExcludesInvalidValues(IList<SetCookieHeaderValue> cookies, string[] input) + { + bool result = SetCookieHeaderValue.TryParseList(input, out var results); + Assert.Equal(cookies, results); + Assert.Equal(cookies?.Count > 0, result); + } + + [Theory] + [MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_ParseStrictList_ThrowsForAnyInvalidValues( +#pragma warning disable xUnit1026 // Theory methods should use all of their parameters + IList<SetCookieHeaderValue> cookies, +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + string[] input) + { + Assert.Throws<FormatException>(() => SetCookieHeaderValue.ParseStrictList(input)); + } + + [Theory] + [MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParseStrictList_FailsForAnyInvalidValues( +#pragma warning disable xUnit1026 // Theory methods should use all of their parameters + IList<SetCookieHeaderValue> cookies, +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + string[] input) + { + bool result = SetCookieHeaderValue.TryParseStrictList(input, out var results); + Assert.Null(results); + Assert.False(result); + } + } +} diff --git a/src/Http/Headers/test/StringWithQualityHeaderValueComparerTest.cs b/src/Http/Headers/test/StringWithQualityHeaderValueComparerTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..8cda48eef3413164af11a7fc45f6c38e03fe848a --- /dev/null +++ b/src/Http/Headers/test/StringWithQualityHeaderValueComparerTest.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class StringWithQualityHeaderValueComparerTest + { + public static TheoryData<string[], string[]> StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues + { + get + { + return new TheoryData<string[], string[]> + { + { + new string[] + { + "text", + "text;q=1.0", + "text", + "text;q=0", + "*;q=0.8", + "*;q=1", + "text;q=0.8", + "*;q=0.6", + "text;q=1.0", + "*;q=0.4", + "text;q=0.6", + }, + new string[] + { + "text", + "text;q=1.0", + "text", + "text;q=1.0", + "*;q=1", + "text;q=0.8", + "*;q=0.8", + "text;q=0.6", + "*;q=0.6", + "*;q=0.4", + "text;q=0", + } + } + }; + } + } + + [Theory] + [MemberData(nameof(StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues))] + public void SortStringWithQualityHeaderValuesByQFactor_SortsCorrectly(IEnumerable<string> unsorted, IEnumerable<string> expectedSorted) + { + var unsortedValues = StringWithQualityHeaderValue.ParseList(unsorted.ToList()); + var expectedSortedValues = StringWithQualityHeaderValue.ParseList(expectedSorted.ToList()); + + var actualSorted = unsortedValues.OrderByDescending(k => k, StringWithQualityHeaderValueComparer.QualityComparer).ToList(); + + Assert.True(expectedSortedValues.SequenceEqual(actualSorted)); + } + } +} diff --git a/src/Http/Headers/test/StringWithQualityHeaderValueTest.cs b/src/Http/Headers/test/StringWithQualityHeaderValueTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..49ee58b93eb388ec6229e5d7fdd5fac3ee5874ee --- /dev/null +++ b/src/Http/Headers/test/StringWithQualityHeaderValueTest.cs @@ -0,0 +1,498 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public class StringWithQualityHeaderValueTest + { + [Fact] + public void Ctor_StringOnlyOverload_MatchExpectation() + { + var value = new StringWithQualityHeaderValue("token"); + Assert.Equal("token", value.Value); + Assert.Null(value.Quality); + + Assert.Throws<ArgumentException>(() => new StringWithQualityHeaderValue(null)); + Assert.Throws<ArgumentException>(() => new StringWithQualityHeaderValue("")); + Assert.Throws<FormatException>(() => new StringWithQualityHeaderValue("in valid")); + } + + [Fact] + public void Ctor_StringWithQualityOverload_MatchExpectation() + { + var value = new StringWithQualityHeaderValue("token", 0.5); + Assert.Equal("token", value.Value); + Assert.Equal(0.5, value.Quality); + + Assert.Throws<ArgumentException>(() => new StringWithQualityHeaderValue(null, 0.1)); + Assert.Throws<ArgumentException>(() => new StringWithQualityHeaderValue("", 0.1)); + Assert.Throws<FormatException>(() => new StringWithQualityHeaderValue("in valid", 0.1)); + + Assert.Throws<ArgumentOutOfRangeException>(() => new StringWithQualityHeaderValue("t", 1.1)); + Assert.Throws<ArgumentOutOfRangeException>(() => new StringWithQualityHeaderValue("t", -0.1)); + } + + [Fact] + public void ToString_UseDifferentValues_AllSerializedCorrectly() + { + var value = new StringWithQualityHeaderValue("token"); + Assert.Equal("token", value.ToString()); + + value = new StringWithQualityHeaderValue("token", 0.1); + Assert.Equal("token; q=0.1", value.ToString()); + + value = new StringWithQualityHeaderValue("token", 0); + Assert.Equal("token; q=0.0", value.ToString()); + + value = new StringWithQualityHeaderValue("token", 1); + Assert.Equal("token; q=1.0", value.ToString()); + + // Note that the quality value gets rounded + value = new StringWithQualityHeaderValue("token", 0.56789); + Assert.Equal("token; q=0.568", value.ToString()); + } + + [Fact] + public void GetHashCode_UseSameAndDifferentValues_SameOrDifferentHashCodes() + { + var value1 = new StringWithQualityHeaderValue("t", 0.123); + var value2 = new StringWithQualityHeaderValue("t", 0.123); + var value3 = new StringWithQualityHeaderValue("T", 0.123); + var value4 = new StringWithQualityHeaderValue("t"); + var value5 = new StringWithQualityHeaderValue("x", 0.123); + var value6 = new StringWithQualityHeaderValue("t", 0.5); + var value7 = new StringWithQualityHeaderValue("t", 0.1234); + var value8 = new StringWithQualityHeaderValue("T"); + var value9 = new StringWithQualityHeaderValue("x"); + + Assert.Equal(value1.GetHashCode(), value2.GetHashCode()); + Assert.Equal(value1.GetHashCode(), value3.GetHashCode()); + Assert.NotEqual(value1.GetHashCode(), value4.GetHashCode()); + Assert.NotEqual(value1.GetHashCode(), value5.GetHashCode()); + Assert.NotEqual(value1.GetHashCode(), value6.GetHashCode()); + Assert.NotEqual(value1.GetHashCode(), value7.GetHashCode()); + Assert.Equal(value4.GetHashCode(), value8.GetHashCode()); + Assert.NotEqual(value4.GetHashCode(), value9.GetHashCode()); + } + + [Fact] + public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() + { + var value1 = new StringWithQualityHeaderValue("t", 0.123); + var value2 = new StringWithQualityHeaderValue("t", 0.123); + var value3 = new StringWithQualityHeaderValue("T", 0.123); + var value4 = new StringWithQualityHeaderValue("t"); + var value5 = new StringWithQualityHeaderValue("x", 0.123); + var value6 = new StringWithQualityHeaderValue("t", 0.5); + var value7 = new StringWithQualityHeaderValue("t", 0.1234); + var value8 = new StringWithQualityHeaderValue("T"); + var value9 = new StringWithQualityHeaderValue("x"); + + Assert.False(value1.Equals(null), "t; q=0.123 vs. <null>"); + Assert.True(value1.Equals(value2), "t; q=0.123 vs. t; q=0.123"); + Assert.True(value1.Equals(value3), "t; q=0.123 vs. T; q=0.123"); + Assert.False(value1.Equals(value4), "t; q=0.123 vs. t"); + Assert.False(value4.Equals(value1), "t vs. t; q=0.123"); + Assert.False(value1.Equals(value5), "t; q=0.123 vs. x; q=0.123"); + Assert.False(value1.Equals(value6), "t; q=0.123 vs. t; q=0.5"); + Assert.False(value1.Equals(value7), "t; q=0.123 vs. t; q=0.1234"); + Assert.True(value4.Equals(value8), "t vs. T"); + Assert.False(value4.Equals(value9), "t vs. T"); + } + + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidParse("text", new StringWithQualityHeaderValue("text")); + CheckValidParse("text;q=0.5", new StringWithQualityHeaderValue("text", 0.5)); + CheckValidParse("text ; q = 0.5", new StringWithQualityHeaderValue("text", 0.5)); + CheckValidParse("\r\n text ; q = 0.5 ", new StringWithQualityHeaderValue("text", 0.5)); + CheckValidParse(" text ", new StringWithQualityHeaderValue("text")); + CheckValidParse(" \r\n text \r\n ; \r\n q = 0.123", new StringWithQualityHeaderValue("text", 0.123)); + CheckValidParse(" text ; q = 0.123 ", new StringWithQualityHeaderValue("text", 0.123)); + CheckValidParse("text;q=1 ", new StringWithQualityHeaderValue("text", 1)); + CheckValidParse("*", new StringWithQualityHeaderValue("*")); + CheckValidParse("*;q=0.7", new StringWithQualityHeaderValue("*", 0.7)); + CheckValidParse(" t", new StringWithQualityHeaderValue("t")); + CheckValidParse("t;q=0.", new StringWithQualityHeaderValue("t", 0)); + CheckValidParse("t;q=1.", new StringWithQualityHeaderValue("t", 1)); + CheckValidParse("t;q=1.000", new StringWithQualityHeaderValue("t", 1)); + CheckValidParse("t;q=0.12345678", new StringWithQualityHeaderValue("t", 0.12345678)); + CheckValidParse("t ; q = 0", new StringWithQualityHeaderValue("t", 0)); + CheckValidParse("iso-8859-5", new StringWithQualityHeaderValue("iso-8859-5")); + CheckValidParse("unicode-1-1; q=0.8", new StringWithQualityHeaderValue("unicode-1-1", 0.8)); + } + + [Theory] + [InlineData("text,")] + [InlineData("\r\n text ; q = 0.5, next_text ")] + [InlineData(" text,next_text ")] + [InlineData(" ,, text, , ,next")] + [InlineData(" ,, text, , ,")] + [InlineData(", \r\n text \r\n ; \r\n q = 0.123")] + [InlineData("teäxt")] + [InlineData("text会")] + [InlineData("会")] + [InlineData("t;q=会")] + [InlineData("t;q=")] + [InlineData("t;q")] + [InlineData("t;会=1")] + [InlineData("t;q会=1")] + [InlineData("t y")] + [InlineData("t;q=1 y")] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ,,")] + [InlineData("t;q=-1")] + [InlineData("t;q=1.00001")] + [InlineData("t;")] + [InlineData("t;;q=1")] + [InlineData("t;q=a")] + [InlineData("t;qa")] + [InlineData("t;q1")] + [InlineData("integer_part_too_long;q=01")] + [InlineData("integer_part_too_long;q=01.0")] + [InlineData("decimal_part_too_long;q=0.123456789")] + [InlineData("decimal_part_too_long;q=0.123456789 ")] + [InlineData("no_integer_part;q=.1")] + public void Parse_SetOfInvalidValueStrings_Throws(string input) + { + Assert.Throws<FormatException>(() => StringWithQualityHeaderValue.Parse(input)); + } + + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidTryParse("text", new StringWithQualityHeaderValue("text")); + CheckValidTryParse("text;q=0.5", new StringWithQualityHeaderValue("text", 0.5)); + CheckValidTryParse("text ; q = 0.5", new StringWithQualityHeaderValue("text", 0.5)); + CheckValidTryParse("\r\n text ; q = 0.5 ", new StringWithQualityHeaderValue("text", 0.5)); + CheckValidTryParse(" text ", new StringWithQualityHeaderValue("text")); + CheckValidTryParse(" \r\n text \r\n ; \r\n q = 0.123", new StringWithQualityHeaderValue("text", 0.123)); + } + + [Fact] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() + { + CheckInvalidTryParse("text,"); + CheckInvalidTryParse("\r\n text ; q = 0.5, next_text "); + CheckInvalidTryParse(" text,next_text "); + CheckInvalidTryParse(" ,, text, , ,next"); + CheckInvalidTryParse(" ,, text, , ,"); + CheckInvalidTryParse(", \r\n text \r\n ; \r\n q = 0.123"); + CheckInvalidTryParse("teäxt"); + CheckInvalidTryParse("text会"); + CheckInvalidTryParse("会"); + CheckInvalidTryParse("t;q=会"); + CheckInvalidTryParse("t;q="); + CheckInvalidTryParse("t;q"); + CheckInvalidTryParse("t;会=1"); + CheckInvalidTryParse("t;q会=1"); + CheckInvalidTryParse("t y"); + CheckInvalidTryParse("t;q=1 y"); + + CheckInvalidTryParse(null); + CheckInvalidTryParse(string.Empty); + CheckInvalidTryParse(" "); + CheckInvalidTryParse(" ,,"); + } + + [Fact] + public void ParseList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] + { + "", + "text1", + "text2,", + "textA,textB", + "text3;q=0.5", + "text4;q=0.5,", + " text5 ; q = 0.50 ", + "\r\n text6 ; q = 0.05 ", + "text7,text8;q=0.5", + " text9 , text10 ; q = 0.5 ", + }; + IList<StringWithQualityHeaderValue> results = StringWithQualityHeaderValue.ParseList(inputs); + + var expectedResults = new[] + { + new StringWithQualityHeaderValue("text1"), + new StringWithQualityHeaderValue("text2"), + new StringWithQualityHeaderValue("textA"), + new StringWithQualityHeaderValue("textB"), + new StringWithQualityHeaderValue("text3", 0.5), + new StringWithQualityHeaderValue("text4", 0.5), + new StringWithQualityHeaderValue("text5", 0.5), + new StringWithQualityHeaderValue("text6", 0.05), + new StringWithQualityHeaderValue("text7"), + new StringWithQualityHeaderValue("text8", 0.5), + new StringWithQualityHeaderValue("text9"), + new StringWithQualityHeaderValue("text10", 0.5), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void ParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] + { + "", + "text1", + "text2,", + "textA,textB", + "text3;q=0.5", + "text4;q=0.5,", + " text5 ; q = 0.50 ", + "\r\n text6 ; q = 0.05 ", + "text7,text8;q=0.5", + " text9 , text10 ; q = 0.5 ", + }; + IList<StringWithQualityHeaderValue> results = StringWithQualityHeaderValue.ParseStrictList(inputs); + + var expectedResults = new[] + { + new StringWithQualityHeaderValue("text1"), + new StringWithQualityHeaderValue("text2"), + new StringWithQualityHeaderValue("textA"), + new StringWithQualityHeaderValue("textB"), + new StringWithQualityHeaderValue("text3", 0.5), + new StringWithQualityHeaderValue("text4", 0.5), + new StringWithQualityHeaderValue("text5", 0.5), + new StringWithQualityHeaderValue("text6", 0.05), + new StringWithQualityHeaderValue("text7"), + new StringWithQualityHeaderValue("text8", 0.5), + new StringWithQualityHeaderValue("text9"), + new StringWithQualityHeaderValue("text10", 0.5), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void TryParseList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] + { + "", + "text1", + "text2,", + "textA,textB", + "text3;q=0.5", + "text4;q=0.5,", + " text5 ; q = 0.50 ", + "\r\n text6 ; q = 0.05 ", + "text7,text8;q=0.5", + " text9 , text10 ; q = 0.5 ", + }; + IList<StringWithQualityHeaderValue> results; + Assert.True(StringWithQualityHeaderValue.TryParseList(inputs, out results)); + + var expectedResults = new[] + { + new StringWithQualityHeaderValue("text1"), + new StringWithQualityHeaderValue("text2"), + new StringWithQualityHeaderValue("textA"), + new StringWithQualityHeaderValue("textB"), + new StringWithQualityHeaderValue("text3", 0.5), + new StringWithQualityHeaderValue("text4", 0.5), + new StringWithQualityHeaderValue("text5", 0.5), + new StringWithQualityHeaderValue("text6", 0.05), + new StringWithQualityHeaderValue("text7"), + new StringWithQualityHeaderValue("text8", 0.5), + new StringWithQualityHeaderValue("text9"), + new StringWithQualityHeaderValue("text10", 0.5), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void TryParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] + { + "", + "text1", + "text2,", + "textA,textB", + "text3;q=0.5", + "text4;q=0.5,", + " text5 ; q = 0.50 ", + "\r\n text6 ; q = 0.05 ", + "text7,text8;q=0.5", + " text9 , text10 ; q = 0.5 ", + }; + IList<StringWithQualityHeaderValue> results; + Assert.True(StringWithQualityHeaderValue.TryParseStrictList(inputs, out results)); + + var expectedResults = new[] + { + new StringWithQualityHeaderValue("text1"), + new StringWithQualityHeaderValue("text2"), + new StringWithQualityHeaderValue("textA"), + new StringWithQualityHeaderValue("textB"), + new StringWithQualityHeaderValue("text3", 0.5), + new StringWithQualityHeaderValue("text4", 0.5), + new StringWithQualityHeaderValue("text5", 0.5), + new StringWithQualityHeaderValue("text6", 0.05), + new StringWithQualityHeaderValue("text7"), + new StringWithQualityHeaderValue("text8", 0.5), + new StringWithQualityHeaderValue("text9"), + new StringWithQualityHeaderValue("text10", 0.5), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void ParseList_WithSomeInvlaidValues_IgnoresInvalidValues() + { + var inputs = new[] + { + "", + "text1", + "text 1", + "text2", + "\"text 2\",", + "text3;q=0.5", + "text4;q=0.5, extra stuff", + " text5 ; q = 0.50 ", + "\r\n text6 ; q = 0.05 ", + "text7,text8;q=0.5", + " text9 , text10 ; q = 0.5 ", + }; + var results = StringWithQualityHeaderValue.ParseList(inputs); + + var expectedResults = new[] + { + new StringWithQualityHeaderValue("text1"), + new StringWithQualityHeaderValue("1"), + new StringWithQualityHeaderValue("text2"), + new StringWithQualityHeaderValue("text3", 0.5), + new StringWithQualityHeaderValue("text4", 0.5), + new StringWithQualityHeaderValue("stuff"), + new StringWithQualityHeaderValue("text5", 0.5), + new StringWithQualityHeaderValue("text6", 0.05), + new StringWithQualityHeaderValue("text7"), + new StringWithQualityHeaderValue("text8", 0.5), + new StringWithQualityHeaderValue("text9"), + new StringWithQualityHeaderValue("text10", 0.5), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void ParseStrictList_WithSomeInvlaidValues_Throws() + { + var inputs = new[] + { + "", + "text1", + "text 1", + "text2", + "\"text 2\",", + "text3;q=0.5", + "text4;q=0.5, extra stuff", + " text5 ; q = 0.50 ", + "\r\n text6 ; q = 0.05 ", + "text7,text8;q=0.5", + " text9 , text10 ; q = 0.5 ", + }; + Assert.Throws<FormatException>(() => StringWithQualityHeaderValue.ParseStrictList(inputs)); + } + + [Fact] + public void TryParseList_WithSomeInvlaidValues_IgnoresInvalidValues() + { + var inputs = new[] + { + "", + "text1", + "text 1", + "text2", + "\"text 2\",", + "text3;q=0.5", + "text4;q=0.5, extra stuff", + " text5 ; q = 0.50 ", + "\r\n text6 ; q = 0.05 ", + "text7,text8;q=0.5", + " text9 , text10 ; q = 0.5 ", + }; + IList<StringWithQualityHeaderValue> results; + Assert.True(StringWithQualityHeaderValue.TryParseList(inputs, out results)); + + var expectedResults = new[] + { + new StringWithQualityHeaderValue("text1"), + new StringWithQualityHeaderValue("1"), + new StringWithQualityHeaderValue("text2"), + new StringWithQualityHeaderValue("text3", 0.5), + new StringWithQualityHeaderValue("text4", 0.5), + new StringWithQualityHeaderValue("stuff"), + new StringWithQualityHeaderValue("text5", 0.5), + new StringWithQualityHeaderValue("text6", 0.05), + new StringWithQualityHeaderValue("text7"), + new StringWithQualityHeaderValue("text8", 0.5), + new StringWithQualityHeaderValue("text9"), + new StringWithQualityHeaderValue("text10", 0.5), + }.ToList(); + + Assert.Equal(expectedResults, results); + } + + [Fact] + public void TryParseStrictList_WithSomeInvlaidValues_ReturnsFalse() + { + var inputs = new[] + { + "", + "text1", + "text 1", + "text2", + "\"text 2\",", + "text3;q=0.5", + "text4;q=0.5, extra stuff", + " text5 ; q = 0.50 ", + "\r\n text6 ; q = 0.05 ", + "text7,text8;q=0.5", + " text9 , text10 ; q = 0.5 ", + }; + IList<StringWithQualityHeaderValue> results; + Assert.False(StringWithQualityHeaderValue.TryParseStrictList(inputs, out results)); + } + + #region Helper methods + + private void CheckValidParse(string input, StringWithQualityHeaderValue expectedResult) + { + var result = StringWithQualityHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } + + private void CheckValidTryParse(string input, StringWithQualityHeaderValue expectedResult) + { + StringWithQualityHeaderValue result = null; + Assert.True(StringWithQualityHeaderValue.TryParse(input, out result)); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidTryParse(string input) + { + StringWithQualityHeaderValue result = null; + Assert.False(StringWithQualityHeaderValue.TryParse(input, out result)); + Assert.Null(result); + } + + #endregion + } +} diff --git a/src/Http/Http.Abstractions/src/Authentication/AuthenticateInfo.cs b/src/Http/Http.Abstractions/src/Authentication/AuthenticateInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..9e8e3fd53760dfec918df29f82196a3c3954c1d9 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Authentication/AuthenticateInfo.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Http.Authentication +{ + /// <summary> + /// Used to store the results of an Authenticate call. + /// </summary> + public class AuthenticateInfo + { + /// <summary> + /// The <see cref="ClaimsPrincipal"/>. + /// </summary> + public ClaimsPrincipal Principal { get; set; } + + /// <summary> + /// The <see cref="AuthenticationProperties"/>. + /// </summary> + public AuthenticationProperties Properties { get; set; } + + /// <summary> + /// The <see cref="AuthenticationDescription"/>. + /// </summary> + public AuthenticationDescription Description { get; set; } + } +} diff --git a/src/Http/Http.Abstractions/src/Authentication/AuthenticationDescription.cs b/src/Http/Http.Abstractions/src/Authentication/AuthenticationDescription.cs new file mode 100644 index 0000000000000000000000000000000000000000..fb0a073f0bbfbf56bf600b140eb53cc838669d02 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Authentication/AuthenticationDescription.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Microsoft.AspNetCore.Http.Authentication +{ + /// <summary> + /// Contains information describing an authentication provider. + /// </summary> + public class AuthenticationDescription + { + private const string DisplayNamePropertyKey = "DisplayName"; + private const string AuthenticationSchemePropertyKey = "AuthenticationScheme"; + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationDescription"/> class + /// </summary> + public AuthenticationDescription() + : this(items: null) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationDescription"/> class + /// </summary> + /// <param name="items"></param> + public AuthenticationDescription(IDictionary<string, object> items) + { + Items = items ?? new Dictionary<string, object>(StringComparer.Ordinal); ; + } + + /// <summary> + /// Contains metadata about the authentication provider. + /// </summary> + public IDictionary<string, object> Items { get; } + + /// <summary> + /// Gets or sets the name used to reference the authentication middleware instance. + /// </summary> + public string AuthenticationScheme + { + get { return GetString(AuthenticationSchemePropertyKey); } + set { Items[AuthenticationSchemePropertyKey] = value; } + } + + /// <summary> + /// Gets or sets the display name for the authentication provider. + /// </summary> + public string DisplayName + { + get { return GetString(DisplayNamePropertyKey); } + set { Items[DisplayNamePropertyKey] = value; } + } + + private string GetString(string name) + { + object value; + if (Items.TryGetValue(name, out value)) + { + return Convert.ToString(value, CultureInfo.InvariantCulture); + } + return null; + } + } +} diff --git a/src/Http/Http.Abstractions/src/Authentication/AuthenticationManager.cs b/src/Http/Http.Abstractions/src/Authentication/AuthenticationManager.cs new file mode 100644 index 0000000000000000000000000000000000000000..b2916522a5e3e209675359bcfa3ce01657c07cb7 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Authentication/AuthenticationManager.cs @@ -0,0 +1,132 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features.Authentication; + +namespace Microsoft.AspNetCore.Http.Authentication +{ + [Obsolete("This is obsolete and will be removed in a future version. See https://go.microsoft.com/fwlink/?linkid=845470.")] + public abstract class AuthenticationManager + { + /// <summary> + /// Constant used to represent the automatic scheme + /// </summary> + public const string AutomaticScheme = "Automatic"; + + public abstract HttpContext HttpContext { get; } + + public abstract IEnumerable<AuthenticationDescription> GetAuthenticationSchemes(); + + public abstract Task<AuthenticateInfo> GetAuthenticateInfoAsync(string authenticationScheme); + + // Will remove once callees have been updated + public abstract Task AuthenticateAsync(AuthenticateContext context); + + public virtual async Task<ClaimsPrincipal> AuthenticateAsync(string authenticationScheme) + { + return (await GetAuthenticateInfoAsync(authenticationScheme))?.Principal; + } + + public virtual Task ChallengeAsync() + { + return ChallengeAsync(properties: null); + } + + public virtual Task ChallengeAsync(AuthenticationProperties properties) + { + return ChallengeAsync(authenticationScheme: AutomaticScheme, properties: properties); + } + + public virtual Task ChallengeAsync(string authenticationScheme) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + + return ChallengeAsync(authenticationScheme: authenticationScheme, properties: null); + } + + // Leave it up to authentication handler to do the right thing for the challenge + public virtual Task ChallengeAsync(string authenticationScheme, AuthenticationProperties properties) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + + return ChallengeAsync(authenticationScheme, properties, ChallengeBehavior.Automatic); + } + + public virtual Task SignInAsync(string authenticationScheme, ClaimsPrincipal principal) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return SignInAsync(authenticationScheme, principal, properties: null); + } + + /// <summary> + /// Creates a challenge for the authentication manager with <see cref="ChallengeBehavior.Forbidden"/>. + /// </summary> + /// <returns>A <see cref="Task"/> that represents the asynchronous challenge operation.</returns> + public virtual Task ForbidAsync() + => ForbidAsync(AutomaticScheme, properties: null); + + public virtual Task ForbidAsync(string authenticationScheme) + { + if (authenticationScheme == null) + { + throw new ArgumentNullException(nameof(authenticationScheme)); + } + + return ForbidAsync(authenticationScheme, properties: null); + } + + // Deny access (typically a 403) + public virtual Task ForbidAsync(string authenticationScheme, AuthenticationProperties properties) + { + if (authenticationScheme == null) + { + throw new ArgumentNullException(nameof(authenticationScheme)); + } + + return ChallengeAsync(authenticationScheme, properties, ChallengeBehavior.Forbidden); + } + + /// <summary> + /// Creates a challenge for the authentication manager with <see cref="ChallengeBehavior.Forbidden"/>. + /// </summary> + /// <param name="properties">Additional arbitrary values which may be used by particular authentication types.</param> + /// <returns>A <see cref="Task"/> that represents the asynchronous challenge operation.</returns> + public virtual Task ForbidAsync(AuthenticationProperties properties) + => ForbidAsync(AutomaticScheme, properties); + + public abstract Task ChallengeAsync(string authenticationScheme, AuthenticationProperties properties, ChallengeBehavior behavior); + + public abstract Task SignInAsync(string authenticationScheme, ClaimsPrincipal principal, AuthenticationProperties properties); + + public virtual Task SignOutAsync(string authenticationScheme) + { + if (authenticationScheme == null) + { + throw new ArgumentNullException(nameof(authenticationScheme)); + } + + return SignOutAsync(authenticationScheme, properties: null); + } + + public abstract Task SignOutAsync(string authenticationScheme, AuthenticationProperties properties); + } +} diff --git a/src/Http/Http.Abstractions/src/Authentication/AuthenticationProperties.cs b/src/Http/Http.Abstractions/src/Authentication/AuthenticationProperties.cs new file mode 100644 index 0000000000000000000000000000000000000000..881b24fff5e7c3f962f3d33a1cdf3d54a8836394 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Authentication/AuthenticationProperties.cs @@ -0,0 +1,197 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Microsoft.AspNetCore.Http.Authentication +{ + /// <summary> + /// Dictionary used to store state values about the authentication session. + /// </summary> + public class AuthenticationProperties + { + internal const string IssuedUtcKey = ".issued"; + internal const string ExpiresUtcKey = ".expires"; + internal const string IsPersistentKey = ".persistent"; + internal const string RedirectUriKey = ".redirect"; + internal const string RefreshKey = ".refresh"; + internal const string UtcDateTimeFormat = "r"; + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationProperties"/> class + /// </summary> + public AuthenticationProperties() + : this(items: null) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationProperties"/> class + /// </summary> + /// <param name="items"></param> + public AuthenticationProperties(IDictionary<string, string> items) + { + Items = items ?? new Dictionary<string, string>(StringComparer.Ordinal); + } + + /// <summary> + /// State values about the authentication session. + /// </summary> + public IDictionary<string, string> Items { get; } + + /// <summary> + /// Gets or sets whether the authentication session is persisted across multiple requests. + /// </summary> + public bool IsPersistent + { + get { return Items.ContainsKey(IsPersistentKey); } + set + { + if (Items.ContainsKey(IsPersistentKey)) + { + if (!value) + { + Items.Remove(IsPersistentKey); + } + } + else + { + if (value) + { + Items.Add(IsPersistentKey, string.Empty); + } + } + } + } + + /// <summary> + /// Gets or sets the full path or absolute URI to be used as an HTTP redirect response value. + /// </summary> + public string RedirectUri + { + get + { + string value; + return Items.TryGetValue(RedirectUriKey, out value) ? value : null; + } + set + { + if (value != null) + { + Items[RedirectUriKey] = value; + } + else + { + if (Items.ContainsKey(RedirectUriKey)) + { + Items.Remove(RedirectUriKey); + } + } + } + } + + /// <summary> + /// Gets or sets the time at which the authentication ticket was issued. + /// </summary> + public DateTimeOffset? IssuedUtc + { + get + { + string value; + if (Items.TryGetValue(IssuedUtcKey, out value)) + { + DateTimeOffset dateTimeOffset; + if (DateTimeOffset.TryParseExact(value, UtcDateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out dateTimeOffset)) + { + return dateTimeOffset; + } + } + return null; + } + set + { + if (value.HasValue) + { + Items[IssuedUtcKey] = value.Value.ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture); + } + else + { + if (Items.ContainsKey(IssuedUtcKey)) + { + Items.Remove(IssuedUtcKey); + } + } + } + } + + /// <summary> + /// Gets or sets the time at which the authentication ticket expires. + /// </summary> + public DateTimeOffset? ExpiresUtc + { + get + { + string value; + if (Items.TryGetValue(ExpiresUtcKey, out value)) + { + DateTimeOffset dateTimeOffset; + if (DateTimeOffset.TryParseExact(value, UtcDateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out dateTimeOffset)) + { + return dateTimeOffset; + } + } + return null; + } + set + { + if (value.HasValue) + { + Items[ExpiresUtcKey] = value.Value.ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture); + } + else + { + if (Items.ContainsKey(ExpiresUtcKey)) + { + Items.Remove(ExpiresUtcKey); + } + } + } + } + + /// <summary> + /// Gets or sets if refreshing the authentication session should be allowed. + /// </summary> + public bool? AllowRefresh + { + get + { + string value; + if (Items.TryGetValue(RefreshKey, out value)) + { + bool refresh; + if (bool.TryParse(value, out refresh)) + { + return refresh; + } + } + return null; + } + set + { + if (value.HasValue) + { + Items[RefreshKey] = value.Value.ToString(); + } + else + { + if (Items.ContainsKey(RefreshKey)) + { + Items.Remove(RefreshKey); + } + } + } + } + } +} diff --git a/src/Http/Http.Abstractions/src/ConnectionInfo.cs b/src/Http/Http.Abstractions/src/ConnectionInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..d4cab49afea28b86b9fec6a6eb0c8f3af5201fd9 --- /dev/null +++ b/src/Http/Http.Abstractions/src/ConnectionInfo.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + public abstract class ConnectionInfo + { + /// <summary> + /// Gets or sets a unique identifier to represent this connection. + /// </summary> + public abstract string Id { get; set; } + + public abstract IPAddress RemoteIpAddress { get; set; } + + public abstract int RemotePort { get; set; } + + public abstract IPAddress LocalIpAddress { get; set; } + + public abstract int LocalPort { get; set; } + + public abstract X509Certificate2 ClientCertificate { get; set; } + + public abstract Task<X509Certificate2> GetClientCertificateAsync(CancellationToken cancellationToken = new CancellationToken()); + } +} diff --git a/src/Http/Http.Abstractions/src/CookieBuilder.cs b/src/Http/Http.Abstractions/src/CookieBuilder.cs new file mode 100644 index 0000000000000000000000000000000000000000..ce89e5b0541963345fcea75d44bd7efd50f9054a --- /dev/null +++ b/src/Http/Http.Abstractions/src/CookieBuilder.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http.Abstractions; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Defines settings used to create a cookie. + /// </summary> + public class CookieBuilder + { + private string _name; + + /// <summary> + /// The name of the cookie. + /// </summary> + public virtual string Name + { + get => _name; + set => _name = !string.IsNullOrEmpty(value) + ? value + : throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(value)); + } + + /// <summary> + /// The cookie path. + /// </summary> + /// <remarks> + /// Determines the value that will set on <seealso cref="CookieOptions.Path"/>. + /// </remarks> + public virtual string Path { get; set; } + + /// <summary> + /// The domain to associate the cookie with. + /// </summary> + /// <remarks> + /// Determines the value that will set on <seealso cref="CookieOptions.Domain"/>. + /// </remarks> + public virtual string Domain { get; set; } + + /// <summary> + /// Indicates whether a cookie is accessible by client-side script. + /// </summary> + /// <remarks> + /// Determines the value that will set on <seealso cref="CookieOptions.HttpOnly"/>. + /// </remarks> + public virtual bool HttpOnly { get; set; } + + /// <summary> + /// The SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.Lax"/> + /// </summary> + /// <remarks> + /// Determines the value that will set on <seealso cref="CookieOptions.SameSite"/>. + /// </remarks> + public virtual SameSiteMode SameSite { get; set; } = SameSiteMode.Lax; + + /// <summary> + /// The policy that will be used to determine <seealso cref="CookieOptions.Secure"/>. + /// This is determined from the <see cref="HttpContext"/> passed to <see cref="Build(HttpContext, DateTimeOffset)"/>. + /// </summary> + public virtual CookieSecurePolicy SecurePolicy { get; set; } + + /// <summary> + /// Gets or sets the lifespan of a cookie. + /// </summary> + public virtual TimeSpan? Expiration { get; set; } + + /// <summary> + /// Gets or sets the max-age for the cookie. + /// </summary> + public virtual TimeSpan? MaxAge { get; set; } + + /// <summary> + /// Indicates if this cookie is essential for the application to function correctly. If true then + /// consent policy checks may be bypassed. The default value is false. + /// </summary> + public virtual bool IsEssential { get; set; } + + /// <summary> + /// Creates the cookie options from the given <paramref name="context"/>. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/>.</param> + /// <returns>The cookie options.</returns> + public CookieOptions Build(HttpContext context) => Build(context, DateTimeOffset.Now); + + /// <summary> + /// Creates the cookie options from the given <paramref name="context"/> with an expiration based on <paramref name="expiresFrom"/> and <see cref="Expiration"/>. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/>.</param> + /// <param name="expiresFrom">The time to use as the base for computing <seealso cref="CookieOptions.Expires" />.</param> + /// <returns>The cookie options.</returns> + public virtual CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new CookieOptions + { + Path = Path ?? "/", + SameSite = SameSite, + HttpOnly = HttpOnly, + MaxAge = MaxAge, + Domain = Domain, + IsEssential = IsEssential, + Secure = SecurePolicy == CookieSecurePolicy.Always || (SecurePolicy == CookieSecurePolicy.SameAsRequest && context.Request.IsHttps), + Expires = Expiration.HasValue ? expiresFrom.Add(Expiration.Value) : default(DateTimeOffset?) + }; + } + } +} diff --git a/src/Http/Http.Abstractions/src/CookieSecurePolicy.cs b/src/Http/Http.Abstractions/src/CookieSecurePolicy.cs new file mode 100644 index 0000000000000000000000000000000000000000..af32d851b0e97ffcd71efbdae94a7fd3c8a73fe3 --- /dev/null +++ b/src/Http/Http.Abstractions/src/CookieSecurePolicy.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Determines how cookie security properties are set. + /// </summary> + public enum CookieSecurePolicy + { + /// <summary> + /// If the URI that provides the cookie is HTTPS, then the cookie will only be returned to the server on + /// subsequent HTTPS requests. Otherwise if the URI that provides the cookie is HTTP, then the cookie will + /// be returned to the server on all HTTP and HTTPS requests. This is the default value because it ensures + /// HTTPS for all authenticated requests on deployed servers, and also supports HTTP for localhost development + /// and for servers that do not have HTTPS support. + /// </summary> + SameAsRequest, + + /// <summary> + /// Secure is always marked true. Use this value when your login page and all subsequent pages + /// requiring the authenticated identity are HTTPS. Local development will also need to be done with HTTPS urls. + /// </summary> + Always, + + /// <summary> + /// Secure is not marked true. Use this value when your login page is HTTPS, but other pages + /// on the site which are HTTP also require authentication information. This setting is not recommended because + /// the authentication information provided with an HTTP request may be observed and used by other computers + /// on your local network or wireless connection. + /// </summary> + None, + } +} diff --git a/src/Http/Http.Abstractions/src/Extensions/HeaderDictionaryExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/HeaderDictionaryExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..5cc06484a28c3d648fdd7ea8537368d2417689ab --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/HeaderDictionaryExtensions.cs @@ -0,0 +1,56 @@ +// 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.Internal; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + public static class HeaderDictionaryExtensions + { + /// <summary> + /// Add new values. Each item remains a separate array entry. + /// </summary> + /// <param name="headers">The <see cref="IHeaderDictionary"/> to use.</param> + /// <param name="key">The header name.</param> + /// <param name="value">The header value.</param> + public static void Append(this IHeaderDictionary headers, string key, StringValues value) + { + ParsingHelpers.AppendHeaderUnmodified(headers, key, value); + } + + /// <summary> + /// Quotes any values containing commas, and then comma joins all of the values with any existing values. + /// </summary> + /// <param name="headers">The <see cref="IHeaderDictionary"/> to use.</param> + /// <param name="key">The header name.</param> + /// <param name="values">The header values.</param> + public static void AppendCommaSeparatedValues(this IHeaderDictionary headers, string key, params string[] values) + { + ParsingHelpers.AppendHeaderJoined(headers, key, values); + } + + /// <summary> + /// Get the associated values from the collection separated into individual values. + /// Quoted values will not be split, and the quotes will be removed. + /// </summary> + /// <param name="headers">The <see cref="IHeaderDictionary"/> to use.</param> + /// <param name="key">The header name.</param> + /// <returns>the associated values from the collection separated into individual values, or StringValues.Empty if the key is not present.</returns> + public static string[] GetCommaSeparatedValues(this IHeaderDictionary headers, string key) + { + return ParsingHelpers.GetHeaderSplit(headers, key).ToArray(); + } + + /// <summary> + /// Quotes any values containing commas, and then comma joins all of the values. + /// </summary> + /// <param name="headers">The <see cref="IHeaderDictionary"/> to use.</param> + /// <param name="key">The header name.</param> + /// <param name="values">The header values.</param> + public static void SetCommaSeparatedValues(this IHeaderDictionary headers, string key, params string[] values) + { + ParsingHelpers.SetHeaderJoined(headers, key, values); + } + } +} diff --git a/src/Http/Http.Abstractions/src/Extensions/HttpResponseWritingExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/HttpResponseWritingExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..0b24a7d4f48bdac959598b100e202e7cb2193f36 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/HttpResponseWritingExtensions.cs @@ -0,0 +1,67 @@ +// 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.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Convenience methods for writing to the response. + /// </summary> + public static class HttpResponseWritingExtensions + { + /// <summary> + /// Writes the given text to the response body. UTF-8 encoding will be used. + /// </summary> + /// <param name="response">The <see cref="HttpResponse"/>.</param> + /// <param name="text">The text to write to the response.</param> + /// <param name="cancellationToken">Notifies when request operations should be cancelled.</param> + /// <returns>A task that represents the completion of the write operation.</returns> + public static Task WriteAsync(this HttpResponse response, string text, CancellationToken cancellationToken = default(CancellationToken)) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + return response.WriteAsync(text, Encoding.UTF8, cancellationToken); + } + + /// <summary> + /// Writes the given text to the response body using the given encoding. + /// </summary> + /// <param name="response">The <see cref="HttpResponse"/>.</param> + /// <param name="text">The text to write to the response.</param> + /// <param name="encoding">The encoding to use.</param> + /// <param name="cancellationToken">Notifies when request operations should be cancelled.</param> + /// <returns>A task that represents the completion of the write operation.</returns> + public static Task WriteAsync(this HttpResponse response, string text, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + + byte[] data = encoding.GetBytes(text); + return response.Body.WriteAsync(data, 0, data.Length, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/MapExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/MapExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..448e2c6f6d20be7e574f21fb8f1231b1b7cd22b0 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/MapExtensions.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Builder.Extensions; + +namespace Microsoft.AspNetCore.Builder +{ + /// <summary> + /// Extension methods for the <see cref="MapMiddleware"/>. + /// </summary> + public static class MapExtensions + { + /// <summary> + /// Branches the request pipeline based on matches of the given request path. If the request path starts with + /// the given path, the branch is executed. + /// </summary> + /// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param> + /// <param name="pathMatch">The request path to match.</param> + /// <param name="configuration">The branch to take for positive path matches.</param> + /// <returns>The <see cref="IApplicationBuilder"/> instance.</returns> + public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + if (pathMatch.HasValue && pathMatch.Value.EndsWith("/", StringComparison.Ordinal)) + { + throw new ArgumentException("The path must not end with a '/'", nameof(pathMatch)); + } + + // create branch + var branchBuilder = app.New(); + configuration(branchBuilder); + var branch = branchBuilder.Build(); + + var options = new MapOptions + { + Branch = branch, + PathMatch = pathMatch, + }; + return app.Use(next => new MapMiddleware(next, options).Invoke); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/MapMiddleware.cs b/src/Http/Http.Abstractions/src/Extensions/MapMiddleware.cs new file mode 100644 index 0000000000000000000000000000000000000000..a4f67ce4a23986699c74335729d751e9714bee5e --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/MapMiddleware.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Builder.Extensions +{ + /// <summary> + /// Respresents a middleware that maps a request path to a sub-request pipeline. + /// </summary> + public class MapMiddleware + { + private readonly RequestDelegate _next; + private readonly MapOptions _options; + + /// <summary> + /// Creates a new instace of <see cref="MapMiddleware"/>. + /// </summary> + /// <param name="next">The delegate representing the next middleware in the request pipeline.</param> + /// <param name="options">The middleware options.</param> + public MapMiddleware(RequestDelegate next, MapOptions options) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _next = next; + _options = options; + } + + /// <summary> + /// Executes the middleware. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> for the current request.</param> + /// <returns>A task that represents the execution of this middleware.</returns> + public async Task Invoke(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + PathString matchedPath; + PathString remainingPath; + + if (context.Request.Path.StartsWithSegments(_options.PathMatch, out matchedPath, out remainingPath)) + { + // Update the path + var path = context.Request.Path; + var pathBase = context.Request.PathBase; + context.Request.PathBase = pathBase.Add(matchedPath); + context.Request.Path = remainingPath; + + try + { + await _options.Branch(context); + } + finally + { + context.Request.PathBase = pathBase; + context.Request.Path = path; + } + } + else + { + await _next(context); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/MapOptions.cs b/src/Http/Http.Abstractions/src/Extensions/MapOptions.cs new file mode 100644 index 0000000000000000000000000000000000000000..60adc743794216779b916387d0f70c0826034d9a --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/MapOptions.cs @@ -0,0 +1,23 @@ +// 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.Extensions +{ + /// <summary> + /// Options for the <see cref="MapMiddleware"/>. + /// </summary> + public class MapOptions + { + /// <summary> + /// The path to match. + /// </summary> + public PathString PathMatch { get; set; } + + /// <summary> + /// The branch taken for a positive match. + /// </summary> + public RequestDelegate Branch { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/MapWhenExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/MapWhenExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..946379df26f29b5244cf523684706da65dd7f801 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/MapWhenExtensions.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Builder.Extensions; + + +namespace Microsoft.AspNetCore.Builder +{ + using Predicate = Func<HttpContext, bool>; + + /// <summary> + /// Extension methods for the <see cref="MapWhenMiddleware"/>. + /// </summary> + public static class MapWhenExtensions + { + /// <summary> + /// Branches the request pipeline based on the result of the given predicate. + /// </summary> + /// <param name="app"></param> + /// <param name="predicate">Invoked with the request environment to determine if the branch should be taken</param> + /// <param name="configuration">Configures a branch to take</param> + /// <returns></returns> + public static IApplicationBuilder MapWhen(this IApplicationBuilder app, Predicate predicate, Action<IApplicationBuilder> configuration) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + // create branch + var branchBuilder = app.New(); + configuration(branchBuilder); + var branch = branchBuilder.Build(); + + // put middleware in pipeline + var options = new MapWhenOptions + { + Predicate = predicate, + Branch = branch, + }; + return app.Use(next => new MapWhenMiddleware(next, options).Invoke); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/MapWhenMiddleware.cs b/src/Http/Http.Abstractions/src/Extensions/MapWhenMiddleware.cs new file mode 100644 index 0000000000000000000000000000000000000000..b012626ba9f912c63078047417fad4ec97b4ebc7 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/MapWhenMiddleware.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.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Builder.Extensions +{ + /// <summary> + /// Respresents a middleware that runs a sub-request pipeline when a given predicate is matched. + /// </summary> + public class MapWhenMiddleware + { + private readonly RequestDelegate _next; + private readonly MapWhenOptions _options; + + /// <summary> + /// Creates a new instance of <see cref="MapWhenMiddleware"/>. + /// </summary> + /// <param name="next">The delegate representing the next middleware in the request pipeline.</param> + /// <param name="options">The middleware options.</param> + public MapWhenMiddleware(RequestDelegate next, MapWhenOptions options) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _next = next; + _options = options; + } + + /// <summary> + /// Executes the middleware. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> for the current request.</param> + /// <returns>A task that represents the execution of this middleware.</returns> + public async Task Invoke(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (_options.Predicate(context)) + { + await _options.Branch(context); + } + else + { + await _next(context); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/MapWhenOptions.cs b/src/Http/Http.Abstractions/src/Extensions/MapWhenOptions.cs new file mode 100644 index 0000000000000000000000000000000000000000..d18eb7f257a18bca4a0a40996883226a83e82af4 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/MapWhenOptions.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Builder.Extensions +{ + /// <summary> + /// Options for the <see cref="MapWhenMiddleware"/>. + /// </summary> + public class MapWhenOptions + { + private Func<HttpContext, bool> _predicate; + + /// <summary> + /// The user callback that determines if the branch should be taken. + /// </summary> + public Func<HttpContext, bool> Predicate + { + get + { + return _predicate; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _predicate = value; + } + } + + /// <summary> + /// The branch taken for a positive match. + /// </summary> + public RequestDelegate Branch { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/RunExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/RunExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..1124043064273e8c1c140502ce748608c1ada4ea --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/RunExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Builder +{ + /// <summary> + /// Extension methods for adding terminal middleware. + /// </summary> + public static class RunExtensions + { + /// <summary> + /// Adds a terminal middleware delegate to the application's request pipeline. + /// </summary> + /// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param> + /// <param name="handler">A delegate that handles the request.</param> + public static void Run(this IApplicationBuilder app, RequestDelegate handler) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + app.Use(_ => handler); + } + } +} diff --git a/src/Http/Http.Abstractions/src/Extensions/UseExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/UseExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..c0c9a0f6e50cc7cafcef36dfb0583167c85748e3 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/UseExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Builder +{ + /// <summary> + /// Extension methods for adding middleware. + /// </summary> + public static class UseExtensions + { + /// <summary> + /// Adds a middleware delegate defined in-line to the application's request pipeline. + /// </summary> + /// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param> + /// <param name="middleware">A function that handles the request or calls the given next function.</param> + /// <returns>The <see cref="IApplicationBuilder"/> instance.</returns> + public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware) + { + return app.Use(next => + { + return context => + { + Func<Task> simpleNext = () => next(context); + return middleware(context, simpleNext); + }; + }); + } + } +} diff --git a/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..c07fe1e9f1cf022a74ce88b33b97f070910b125e --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs @@ -0,0 +1,224 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Abstractions; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Builder +{ + /// <summary> + /// Extension methods for adding typed middleware. + /// </summary> + public static class UseMiddlewareExtensions + { + internal const string InvokeMethodName = "Invoke"; + internal const string InvokeAsyncMethodName = "InvokeAsync"; + + private static readonly MethodInfo GetServiceInfo = typeof(UseMiddlewareExtensions).GetMethod(nameof(GetService), BindingFlags.NonPublic | BindingFlags.Static); + + /// <summary> + /// Adds a middleware type to the application's request pipeline. + /// </summary> + /// <typeparam name="TMiddleware">The middleware type.</typeparam> + /// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param> + /// <param name="args">The arguments to pass to the middleware type instance's constructor.</param> + /// <returns>The <see cref="IApplicationBuilder"/> instance.</returns> + public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object[] args) + { + return app.UseMiddleware(typeof(TMiddleware), args); + } + + /// <summary> + /// Adds a middleware type to the application's request pipeline. + /// </summary> + /// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param> + /// <param name="middleware">The middleware type.</param> + /// <param name="args">The arguments to pass to the middleware type instance's constructor.</param> + /// <returns>The <see cref="IApplicationBuilder"/> instance.</returns> + public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args) + { + if (typeof(IMiddleware).GetTypeInfo().IsAssignableFrom(middleware.GetTypeInfo())) + { + // IMiddleware doesn't support passing args directly since it's + // activated from the container + if (args.Length > 0) + { + throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware))); + } + + return UseMiddlewareInterface(app, middleware); + } + + var applicationServices = app.ApplicationServices; + return app.Use(next => + { + var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public); + var invokeMethods = methods.Where(m => + string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal) + || string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal) + ).ToArray(); + + if (invokeMethods.Length > 1) + { + throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName)); + } + + if (invokeMethods.Length == 0) + { + throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware)); + } + + var methodinfo = invokeMethods[0]; + if (!typeof(Task).IsAssignableFrom(methodinfo.ReturnType)) + { + throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task))); + } + + var parameters = methodinfo.GetParameters(); + if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext)) + { + throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext))); + } + + var ctorArgs = new object[args.Length + 1]; + ctorArgs[0] = next; + Array.Copy(args, 0, ctorArgs, 1, args.Length); + var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs); + if (parameters.Length == 1) + { + return (RequestDelegate)methodinfo.CreateDelegate(typeof(RequestDelegate), instance); + } + + var factory = Compile<object>(methodinfo, parameters); + + return context => + { + var serviceProvider = context.RequestServices ?? applicationServices; + if (serviceProvider == null) + { + throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider))); + } + + return factory(instance, context, serviceProvider); + }; + }); + } + + private static IApplicationBuilder UseMiddlewareInterface(IApplicationBuilder app, Type middlewareType) + { + return app.Use(next => + { + return async context => + { + var middlewareFactory = (IMiddlewareFactory)context.RequestServices.GetService(typeof(IMiddlewareFactory)); + if (middlewareFactory == null) + { + // No middleware factory + throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory))); + } + + var middleware = middlewareFactory.Create(middlewareType); + if (middleware == null) + { + // The factory returned null, it's a broken implementation + throw new InvalidOperationException(Resources.FormatException_UseMiddlewareUnableToCreateMiddleware(middlewareFactory.GetType(), middlewareType)); + } + + try + { + await middleware.InvokeAsync(context, next); + } + finally + { + middlewareFactory.Release(middleware); + } + }; + }); + } + + private static Func<T, HttpContext, IServiceProvider, Task> Compile<T>(MethodInfo methodinfo, ParameterInfo[] parameters) + { + // If we call something like + // + // public class Middleware + // { + // public Task Invoke(HttpContext context, ILoggerFactory loggeryFactory) + // { + // + // } + // } + // + + // We'll end up with something like this: + // Generic version: + // + // Task Invoke(Middleware instance, HttpContext httpContext, IServiceprovider provider) + // { + // return instance.Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory)); + // } + + // Non generic version: + // + // Task Invoke(object instance, HttpContext httpContext, IServiceprovider provider) + // { + // return ((Middleware)instance).Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory)); + // } + + var middleware = typeof(T); + + var httpContextArg = Expression.Parameter(typeof(HttpContext), "httpContext"); + var providerArg = Expression.Parameter(typeof(IServiceProvider), "serviceProvider"); + var instanceArg = Expression.Parameter(middleware, "middleware"); + + var methodArguments = new Expression[parameters.Length]; + methodArguments[0] = httpContextArg; + for (int i = 1; i < parameters.Length; i++) + { + var parameterType = parameters[i].ParameterType; + if (parameterType.IsByRef) + { + throw new NotSupportedException(Resources.FormatException_InvokeDoesNotSupportRefOrOutParams(InvokeMethodName)); + } + + var parameterTypeExpression = new Expression[] + { + providerArg, + Expression.Constant(parameterType, typeof(Type)), + Expression.Constant(methodinfo.DeclaringType, typeof(Type)) + }; + + var getServiceCall = Expression.Call(GetServiceInfo, parameterTypeExpression); + methodArguments[i] = Expression.Convert(getServiceCall, parameterType); + } + + Expression middlewareInstanceArg = instanceArg; + if (methodinfo.DeclaringType != typeof(T)) + { + middlewareInstanceArg = Expression.Convert(middlewareInstanceArg, methodinfo.DeclaringType); + } + + var body = Expression.Call(middlewareInstanceArg, methodinfo, methodArguments); + + var lambda = Expression.Lambda<Func<T, HttpContext, IServiceProvider, Task>>(body, instanceArg, httpContextArg, providerArg); + + return lambda.Compile(); + } + + private static object GetService(IServiceProvider sp, Type type, Type middleware) + { + var service = sp.GetService(type); + if (service == null) + { + throw new InvalidOperationException(Resources.FormatException_InvokeMiddlewareNoService(type, middleware)); + } + + return service; + } + } +} diff --git a/src/Http/Http.Abstractions/src/Extensions/UsePathBaseExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..482f2f481fe7a1e603aa36ee68e1148cb43e2742 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Builder.Extensions; + +namespace Microsoft.AspNetCore.Builder +{ + /// <summary> + /// Extension methods for <see cref="IApplicationBuilder"/>. + /// </summary> + public static class UsePathBaseExtensions + { + /// <summary> + /// Adds a middleware that extracts the specified path base from request path and postpend it to the request path base. + /// </summary> + /// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param> + /// <param name="pathBase">The path base to extract.</param> + /// <returns>The <see cref="IApplicationBuilder"/> instance.</returns> + public static IApplicationBuilder UsePathBase(this IApplicationBuilder app, PathString pathBase) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + // Strip trailing slashes + pathBase = pathBase.Value?.TrimEnd('/'); + if (!pathBase.HasValue) + { + return app; + } + + return app.UseMiddleware<UsePathBaseMiddleware>(pathBase); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/UsePathBaseMiddleware.cs b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseMiddleware.cs new file mode 100644 index 0000000000000000000000000000000000000000..6474aeda580c3d8a03bd4d2dffd4b01f67c5ea1f --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseMiddleware.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Builder.Extensions +{ + /// <summary> + /// Represents a middleware that extracts the specified path base from request path and postpend it to the request path base. + /// </summary> + public class UsePathBaseMiddleware + { + private readonly RequestDelegate _next; + private readonly PathString _pathBase; + + /// <summary> + /// Creates a new instace of <see cref="UsePathBaseMiddleware"/>. + /// </summary> + /// <param name="next">The delegate representing the next middleware in the request pipeline.</param> + /// <param name="pathBase">The path base to extract.</param> + public UsePathBaseMiddleware(RequestDelegate next, PathString pathBase) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (!pathBase.HasValue) + { + throw new ArgumentException($"{nameof(pathBase)} cannot be null or empty."); + } + + _next = next; + _pathBase = pathBase; + } + + /// <summary> + /// Executes the middleware. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> for the current request.</param> + /// <returns>A task that represents the execution of this middleware.</returns> + public async Task Invoke(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + PathString matchedPath; + PathString remainingPath; + + if (context.Request.Path.StartsWithSegments(_pathBase, out matchedPath, out remainingPath)) + { + var originalPath = context.Request.Path; + var originalPathBase = context.Request.PathBase; + context.Request.Path = remainingPath; + context.Request.PathBase = originalPathBase.Add(matchedPath); + + try + { + await _next(context); + } + finally + { + context.Request.Path = originalPath; + context.Request.PathBase = originalPathBase; + } + } + else + { + await _next(context); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/UseWhenExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/UseWhenExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..f506c41c89412ef4e2ab829a3840d8daf1ee79b8 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/UseWhenExtensions.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Builder +{ + using Predicate = Func<HttpContext, bool>; + + /// <summary> + /// Extension methods for <see cref="IApplicationBuilder"/>. + /// </summary> + public static class UseWhenExtensions + { + /// <summary> + /// Conditionally creates a branch in the request pipeline that is rejoined to the main pipeline. + /// </summary> + /// <param name="app"></param> + /// <param name="predicate">Invoked with the request environment to determine if the branch should be taken</param> + /// <param name="configuration">Configures a branch to take</param> + /// <returns></returns> + public static IApplicationBuilder UseWhen(this IApplicationBuilder app, Predicate predicate, Action<IApplicationBuilder> configuration) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + // Create and configure the branch builder right away; otherwise, + // we would end up running our branch after all the components + // that were subsequently added to the main builder. + var branchBuilder = app.New(); + configuration(branchBuilder); + + return app.Use(main => + { + // This is called only when the main application builder + // is built, not per request. + branchBuilder.Run(main); + var branch = branchBuilder.Build(); + + return context => + { + if (predicate(context)) + { + return branch(context); + } + else + { + return main(context); + } + }; + }); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/FragmentString.cs b/src/Http/Http.Abstractions/src/FragmentString.cs new file mode 100644 index 0000000000000000000000000000000000000000..c1cb30614960cc55cec1d5ff94d7e7719a601804 --- /dev/null +++ b/src/Http/Http.Abstractions/src/FragmentString.cs @@ -0,0 +1,141 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Provides correct handling for FragmentString value when needed to generate a URI string + /// </summary> + public struct FragmentString : IEquatable<FragmentString> + { + /// <summary> + /// Represents the empty fragment string. This field is read-only. + /// </summary> + public static readonly FragmentString Empty = new FragmentString(string.Empty); + + private readonly string _value; + + /// <summary> + /// Initialize the fragment string with a given value. This value must be in escaped and delimited format with + /// a leading '#' character. + /// </summary> + /// <param name="value">The fragment string to be assigned to the Value property.</param> + public FragmentString(string value) + { + if (!string.IsNullOrEmpty(value) && value[0] != '#') + { + throw new ArgumentException("The leading '#' must be included for a non-empty fragment.", nameof(value)); + } + _value = value; + } + + /// <summary> + /// The escaped fragment string with the leading '#' character + /// </summary> + public string Value + { + get { return _value; } + } + + /// <summary> + /// True if the fragment string is not empty + /// </summary> + public bool HasValue + { + get { return !string.IsNullOrEmpty(_value); } + } + + /// <summary> + /// Provides the fragment string escaped in a way which is correct for combining into the URI representation. + /// A leading '#' character will be included unless the Value is null or empty. Characters which are potentially + /// dangerous are escaped. + /// </summary> + /// <returns>The fragment string value</returns> + public override string ToString() + { + return ToUriComponent(); + } + + /// <summary> + /// Provides the fragment string escaped in a way which is correct for combining into the URI representation. + /// A leading '#' character will be included unless the Value is null or empty. Characters which are potentially + /// dangerous are escaped. + /// </summary> + /// <returns>The fragment string value</returns> + public string ToUriComponent() + { + // Escape things properly so System.Uri doesn't mis-interpret the data. + return HasValue ? _value : string.Empty; + } + + /// <summary> + /// Returns an FragmentString given the fragment as it is escaped in the URI format. The string MUST NOT contain any + /// value that is not a fragment. + /// </summary> + /// <param name="uriComponent">The escaped fragment as it appears in the URI format.</param> + /// <returns>The resulting FragmentString</returns> + public static FragmentString FromUriComponent(string uriComponent) + { + if (String.IsNullOrEmpty(uriComponent)) + { + return Empty; + } + return new FragmentString(uriComponent); + } + + /// <summary> + /// Returns an FragmentString given the fragment as from a Uri object. Relative Uri objects are not supported. + /// </summary> + /// <param name="uri">The Uri object</param> + /// <returns>The resulting FragmentString</returns> + public static FragmentString FromUriComponent(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + string fragmentValue = uri.GetComponents(UriComponents.Fragment, UriFormat.UriEscaped); + if (!string.IsNullOrEmpty(fragmentValue)) + { + fragmentValue = "#" + fragmentValue; + } + return new FragmentString(fragmentValue); + } + + public bool Equals(FragmentString other) + { + if (!HasValue && !other.HasValue) + { + return true; + } + return string.Equals(_value, other._value, StringComparison.Ordinal); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return !HasValue; + } + return obj is FragmentString && Equals((FragmentString)obj); + } + + public override int GetHashCode() + { + return (HasValue ? _value.GetHashCode() : 0); + } + + public static bool operator ==(FragmentString left, FragmentString right) + { + return left.Equals(right); + } + + public static bool operator !=(FragmentString left, FragmentString right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Http/Http.Abstractions/src/HostString.cs b/src/Http/Http.Abstractions/src/HostString.cs new file mode 100644 index 0000000000000000000000000000000000000000..9496b26bacee1555097155cc5a2a2f0cf8aa0676 --- /dev/null +++ b/src/Http/Http.Abstractions/src/HostString.cs @@ -0,0 +1,379 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.AspNetCore.Http.Abstractions; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Represents the host portion of a URI can be used to construct URI's properly formatted and encoded for use in + /// HTTP headers. + /// </summary> + public struct HostString : IEquatable<HostString> + { + private readonly string _value; + + /// <summary> + /// Creates a new HostString without modification. The value should be Unicode rather than punycode, and may have a port. + /// IPv4 and IPv6 addresses are also allowed, and also may have ports. + /// </summary> + /// <param name="value"></param> + public HostString(string value) + { + _value = value; + } + + /// <summary> + /// Creates a new HostString from its host and port parts. + /// </summary> + /// <param name="host">The value should be Unicode rather than punycode. IPv6 addresses must use square braces.</param> + /// <param name="port">A positive, greater than 0 value representing the port in the host string.</param> + public HostString(string host, int port) + { + if(port <= 0) + { + throw new ArgumentOutOfRangeException(nameof(port), Resources.Exception_PortMustBeGreaterThanZero); + } + + int index; + if (host.IndexOf('[') == -1 + && (index = host.IndexOf(':')) >= 0 + && index < host.Length - 1 + && host.IndexOf(':', index + 1) >= 0) + { + // IPv6 without brackets ::1 is the only type of host with 2 or more colons + host = $"[{host}]"; + } + + _value = host + ":" + port.ToString(CultureInfo.InvariantCulture); + } + + /// <summary> + /// Returns the original value from the constructor. + /// </summary> + public string Value + { + get { return _value; } + } + + public bool HasValue + { + get { return !string.IsNullOrEmpty(_value); } + } + + /// <summary> + /// Returns the value of the host part of the value. The port is removed if it was present. + /// IPv6 addresses will have brackets added if they are missing. + /// </summary> + /// <returns></returns> + public string Host + { + get + { + GetParts(_value, out var host, out var port); + + return host.ToString(); + } + } + + /// <summary> + /// Returns the value of the port part of the host, or <value>null</value> if none is found. + /// </summary> + /// <returns></returns> + public int? Port + { + get + { + GetParts(_value, out var host, out var port); + + if (!StringSegment.IsNullOrEmpty(port) + && int.TryParse(port.ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out var p)) + { + return p; + } + + return null; + } + } + + /// <summary> + /// Returns the value as normalized by ToUriComponent(). + /// </summary> + /// <returns></returns> + public override string ToString() + { + return ToUriComponent(); + } + + /// <summary> + /// Returns the value properly formatted and encoded for use in a URI in a HTTP header. + /// Any Unicode is converted to punycode. IPv6 addresses will have brackets added if they are missing. + /// </summary> + /// <returns></returns> + public string ToUriComponent() + { + if (string.IsNullOrEmpty(_value)) + { + return string.Empty; + } + + int i; + for (i = 0; i < _value.Length; ++i) + { + if (!HostStringHelper.IsSafeHostStringChar(_value[i])) + { + break; + } + } + + if (i != _value.Length) + { + GetParts(_value, out var host, out var port); + + var mapping = new IdnMapping(); + var encoded = mapping.GetAscii(host.Buffer, host.Offset, host.Length); + + return StringSegment.IsNullOrEmpty(port) + ? encoded + : string.Concat(encoded, ":", port.ToString()); + } + + return _value; + } + + /// <summary> + /// Creates a new HostString from the given URI component. + /// Any punycode will be converted to Unicode. + /// </summary> + /// <param name="uriComponent"></param> + /// <returns></returns> + public static HostString FromUriComponent(string uriComponent) + { + if (!string.IsNullOrEmpty(uriComponent)) + { + int index; + if (uriComponent.IndexOf('[') >= 0) + { + // IPv6 in brackets [::1], maybe with port + } + else if ((index = uriComponent.IndexOf(':')) >= 0 + && index < uriComponent.Length - 1 + && uriComponent.IndexOf(':', index + 1) >= 0) + { + // IPv6 without brackets ::1 is the only type of host with 2 or more colons + } + else if (uriComponent.IndexOf("xn--", StringComparison.Ordinal) >= 0) + { + // Contains punycode + if (index >= 0) + { + // Has a port + string port = uriComponent.Substring(index); + var mapping = new IdnMapping(); + uriComponent = mapping.GetUnicode(uriComponent, 0, index) + port; + } + else + { + var mapping = new IdnMapping(); + uriComponent = mapping.GetUnicode(uriComponent); + } + } + } + return new HostString(uriComponent); + } + + /// <summary> + /// Creates a new HostString from the host and port of the give Uri instance. + /// Punycode will be converted to Unicode. + /// </summary> + /// <param name="uri"></param> + /// <returns></returns> + public static HostString FromUriComponent(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + return new HostString(uri.GetComponents( + UriComponents.NormalizedHost | // Always convert punycode to Unicode. + UriComponents.HostAndPort, UriFormat.Unescaped)); + } + + /// <summary> + /// Matches the host portion of a host header value against a list of patterns. + /// The host may be the encoded punycode or decoded unicode form so long as the pattern + /// uses the same format. + /// </summary> + /// <param name="value">Host header value with or without a port.</param> + /// <param name="patterns">A set of pattern to match, without ports.</param> + /// <remarks> + /// The port on the given value is ignored. The patterns should not have ports. + /// The patterns may be exact matches like "example.com", a top level wildcard "*" + /// that matches all hosts, or a subdomain wildcard like "*.example.com" that matches + /// "abc.example.com:443" but not "example.com:443". + /// Matching is case insensitive. + /// </remarks> + /// <returns></returns> + public static bool MatchesAny(StringSegment value, IList<StringSegment> patterns) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + if (patterns == null) + { + throw new ArgumentNullException(nameof(patterns)); + } + + // Drop the port + GetParts(value, out var host, out var port); + + for (int i = 0; i < port.Length; i++) + { + if (port[i] < '0' || '9' < port[i]) + { + throw new FormatException($"The given host value '{value}' has a malformed port."); + } + } + + for (int i = 0; i < patterns.Count; i++) + { + var pattern = patterns[i]; + + if (pattern == "*") + { + return true; + } + + if (StringSegment.Equals(pattern, host, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Sub-domain wildcards: *.example.com + if (pattern.StartsWith("*.", StringComparison.Ordinal) && host.Length >= pattern.Length) + { + // .example.com + var allowedRoot = pattern.Subsegment(1); + + var hostRoot = host.Subsegment(host.Length - allowedRoot.Length); + if (hostRoot.Equals(allowedRoot, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + return false; + } + + /// <summary> + /// Compares the equality of the Value property, ignoring case. + /// </summary> + /// <param name="other"></param> + /// <returns></returns> + public bool Equals(HostString other) + { + if (!HasValue && !other.HasValue) + { + return true; + } + return string.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase); + } + + /// <summary> + /// Compares against the given object only if it is a HostString. + /// </summary> + /// <param name="obj"></param> + /// <returns></returns> + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return !HasValue; + } + return obj is HostString && Equals((HostString)obj); + } + + /// <summary> + /// Gets a hash code for the value. + /// </summary> + /// <returns></returns> + public override int GetHashCode() + { + return (HasValue ? StringComparer.OrdinalIgnoreCase.GetHashCode(_value) : 0); + } + + /// <summary> + /// Compares the two instances for equality. + /// </summary> + /// <param name="left"></param> + /// <param name="right"></param> + /// <returns></returns> + public static bool operator ==(HostString left, HostString right) + { + return left.Equals(right); + } + + /// <summary> + /// Compares the two instances for inequality. + /// </summary> + /// <param name="left"></param> + /// <param name="right"></param> + /// <returns></returns> + public static bool operator !=(HostString left, HostString right) + { + return !left.Equals(right); + } + + /// <summary> + /// Parses the current value. IPv6 addresses will have brackets added if they are missing. + /// </summary> + private static void GetParts(StringSegment value, out StringSegment host, out StringSegment port) + { + int index; + port = null; + host = null; + + if (StringSegment.IsNullOrEmpty(value)) + { + return; + } + else if ((index = value.IndexOf(']')) >= 0) + { + // IPv6 in brackets [::1], maybe with port + host = value.Subsegment(0, index + 1); + // Is there a colon and at least one character? + if (index + 2 < value.Length && value[index + 1] == ':') + { + port = value.Subsegment(index + 2); + } + } + else if ((index = value.IndexOf(':')) >= 0 + && index < value.Length - 1 + && value.IndexOf(':', index + 1) >= 0) + { + // IPv6 without brackets ::1 is the only type of host with 2 or more colons + host = $"[{value}]"; + port = null; + } + else if (index >= 0) + { + // Has a port + host = value.Subsegment(0, index); + port = value.Subsegment(index + 1); + } + else + { + host = value; + port = null; + } + } + } +} diff --git a/src/Http/Http.Abstractions/src/HttpContext.cs b/src/Http/Http.Abstractions/src/HttpContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..60c938db0a13a714958b3175b3215cae3639f754 --- /dev/null +++ b/src/Http/Http.Abstractions/src/HttpContext.cs @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading; +using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Encapsulates all HTTP-specific information about an individual HTTP request. + /// </summary> + public abstract class HttpContext + { + /// <summary> + /// Gets the collection of HTTP features provided by the server and middleware available on this request. + /// </summary> + public abstract IFeatureCollection Features { get; } + + /// <summary> + /// Gets the <see cref="HttpRequest"/> object for this request. + /// </summary> + public abstract HttpRequest Request { get; } + + /// <summary> + /// Gets the <see cref="HttpResponse"/> object for this request. + /// </summary> + public abstract HttpResponse Response { get; } + + /// <summary> + /// Gets information about the underlying connection for this request. + /// </summary> + public abstract ConnectionInfo Connection { get; } + + /// <summary> + /// Gets an object that manages the establishment of WebSocket connections for this request. + /// </summary> + public abstract WebSocketManager WebSockets { get; } + + /// <summary> + /// This is obsolete and will be removed in a future version. + /// The recommended alternative is to use Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions. + /// See https://go.microsoft.com/fwlink/?linkid=845470. + /// </summary> + [Obsolete("This is obsolete and will be removed in a future version. The recommended alternative is to use Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions. See https://go.microsoft.com/fwlink/?linkid=845470.")] + public abstract AuthenticationManager Authentication { get; } + + /// <summary> + /// Gets or sets the user for this request. + /// </summary> + public abstract ClaimsPrincipal User { get; set; } + + /// <summary> + /// Gets or sets a key/value collection that can be used to share data within the scope of this request. + /// </summary> + public abstract IDictionary<object, object> Items { get; set; } + + /// <summary> + /// Gets or sets the <see cref="IServiceProvider"/> that provides access to the request's service container. + /// </summary> + public abstract IServiceProvider RequestServices { get; set; } + + /// <summary> + /// Notifies when the connection underlying this request is aborted and thus request operations should be + /// cancelled. + /// </summary> + public abstract CancellationToken RequestAborted { get; set; } + + /// <summary> + /// Gets or sets a unique identifier to represent this request in trace logs. + /// </summary> + public abstract string TraceIdentifier { get; set; } + + /// <summary> + /// Gets or sets the object used to manage user session data for this request. + /// </summary> + public abstract ISession Session { get; set; } + + /// <summary> + /// Aborts the connection underlying this request. + /// </summary> + public abstract void Abort(); + } +} diff --git a/src/Http/Http.Abstractions/src/HttpMethods.cs b/src/Http/Http.Abstractions/src/HttpMethods.cs new file mode 100644 index 0000000000000000000000000000000000000000..1ccee896e7020abe2bd65a2f8e9f0b55661af516 --- /dev/null +++ b/src/Http/Http.Abstractions/src/HttpMethods.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Http +{ + public static class HttpMethods + { + public static readonly string Connect = "CONNECT"; + public static readonly string Delete = "DELETE"; + public static readonly string Get = "GET"; + public static readonly string Head = "HEAD"; + public static readonly string Options = "OPTIONS"; + public static readonly string Patch = "PATCH"; + public static readonly string Post = "POST"; + public static readonly string Put = "PUT"; + public static readonly string Trace = "TRACE"; + + public static bool IsConnect(string method) + { + return object.ReferenceEquals(Connect, method) || StringComparer.OrdinalIgnoreCase.Equals(Connect, method); + } + + public static bool IsDelete(string method) + { + return object.ReferenceEquals(Delete, method) || StringComparer.OrdinalIgnoreCase.Equals(Delete, method); + } + + public static bool IsGet(string method) + { + return object.ReferenceEquals(Get, method) || StringComparer.OrdinalIgnoreCase.Equals(Get, method); + } + + public static bool IsHead(string method) + { + return object.ReferenceEquals(Head, method) || StringComparer.OrdinalIgnoreCase.Equals(Head, method); + } + + public static bool IsOptions(string method) + { + return object.ReferenceEquals(Options, method) || StringComparer.OrdinalIgnoreCase.Equals(Options, method); + } + + public static bool IsPatch(string method) + { + return object.ReferenceEquals(Patch, method) || StringComparer.OrdinalIgnoreCase.Equals(Patch, method); + } + + public static bool IsPost(string method) + { + return object.ReferenceEquals(Post, method) || StringComparer.OrdinalIgnoreCase.Equals(Post, method); + } + + public static bool IsPut(string method) + { + return object.ReferenceEquals(Put, method) || StringComparer.OrdinalIgnoreCase.Equals(Put, method); + } + + public static bool IsTrace(string method) + { + return object.ReferenceEquals(Trace, method) || StringComparer.OrdinalIgnoreCase.Equals(Trace, method); + } + } +} diff --git a/src/Http/Http.Abstractions/src/HttpRequest.cs b/src/Http/Http.Abstractions/src/HttpRequest.cs new file mode 100644 index 0000000000000000000000000000000000000000..a4337b77664c554a60bb42b237a8f1d1c36e6882 --- /dev/null +++ b/src/Http/Http.Abstractions/src/HttpRequest.cs @@ -0,0 +1,121 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Represents the incoming side of an individual HTTP request. + /// </summary> + public abstract class HttpRequest + { + /// <summary> + /// Gets the <see cref="HttpContext"/> for this request. + /// </summary> + public abstract HttpContext HttpContext { get; } + + /// <summary> + /// Gets or sets the HTTP method. + /// </summary> + /// <returns>The HTTP method.</returns> + public abstract string Method { get; set; } + + /// <summary> + /// Gets or sets the HTTP request scheme. + /// </summary> + /// <returns>The HTTP request scheme.</returns> + public abstract string Scheme { get; set; } + + /// <summary> + /// Returns true if the RequestScheme is https. + /// </summary> + /// <returns>true if this request is using https; otherwise, false.</returns> + public abstract bool IsHttps { get; set; } + + /// <summary> + /// Gets or sets the Host header. May include the port. + /// </summary> + /// <return>The Host header.</return> + public abstract HostString Host { get; set; } + + /// <summary> + /// Gets or sets the RequestPathBase. + /// </summary> + /// <returns>The RequestPathBase.</returns> + public abstract PathString PathBase { get; set; } + + /// <summary> + /// Gets or sets the request path from RequestPath. + /// </summary> + /// <returns>The request path from RequestPath.</returns> + public abstract PathString Path { get; set; } + + /// <summary> + /// Gets or sets the raw query string used to create the query collection in Request.Query. + /// </summary> + /// <returns>The raw query string.</returns> + public abstract QueryString QueryString { get; set; } + + /// <summary> + /// Gets the query value collection parsed from Request.QueryString. + /// </summary> + /// <returns>The query value collection parsed from Request.QueryString.</returns> + public abstract IQueryCollection Query { get; set; } + + /// <summary> + /// Gets or sets the RequestProtocol. + /// </summary> + /// <returns>The RequestProtocol.</returns> + public abstract string Protocol { get; set; } + + /// <summary> + /// Gets the request headers. + /// </summary> + /// <returns>The request headers.</returns> + public abstract IHeaderDictionary Headers { get; } + + /// <summary> + /// Gets the collection of Cookies for this request. + /// </summary> + /// <returns>The collection of Cookies for this request.</returns> + public abstract IRequestCookieCollection Cookies { get; set; } + + /// <summary> + /// Gets or sets the Content-Length header. + /// </summary> + /// <returns>The value of the Content-Length header, if any.</returns> + public abstract long? ContentLength { get; set; } + + /// <summary> + /// Gets or sets the Content-Type header. + /// </summary> + /// <returns>The Content-Type header.</returns> + public abstract string ContentType { get; set; } + + /// <summary> + /// Gets or sets the RequestBody Stream. + /// </summary> + /// <returns>The RequestBody Stream.</returns> + public abstract Stream Body { get; set; } + + /// <summary> + /// Checks the Content-Type header for form types. + /// </summary> + /// <returns>true if the Content-Type header represents a form content type; otherwise, false.</returns> + public abstract bool HasFormContentType { get; } + + /// <summary> + /// Gets or sets the request body as a form. + /// </summary> + public abstract IFormCollection Form { get; set; } + + /// <summary> + /// Reads the request body if it is a form. + /// </summary> + /// <returns></returns> + public abstract Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken()); + } +} diff --git a/src/Http/Http.Abstractions/src/HttpResponse.cs b/src/Http/Http.Abstractions/src/HttpResponse.cs new file mode 100644 index 0000000000000000000000000000000000000000..8a1e5d49082904198c0e36f4df36ddeeb4b8425f --- /dev/null +++ b/src/Http/Http.Abstractions/src/HttpResponse.cs @@ -0,0 +1,109 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Represents the outgoing side of an individual HTTP request. + /// </summary> + public abstract class HttpResponse + { + private static readonly Func<object, Task> _callbackDelegate = callback => ((Func<Task>)callback)(); + private static readonly Func<object, Task> _disposeDelegate = disposable => + { + ((IDisposable)disposable).Dispose(); + return Task.CompletedTask; + }; + + /// <summary> + /// Gets the <see cref="HttpContext"/> for this response. + /// </summary> + public abstract HttpContext HttpContext { get; } + + /// <summary> + /// Gets or sets the HTTP response code. + /// </summary> + public abstract int StatusCode { get; set; } + + /// <summary> + /// Gets the response headers. + /// </summary> + public abstract IHeaderDictionary Headers { get; } + + /// <summary> + /// Gets or sets the response body <see cref="Stream"/>. + /// </summary> + public abstract Stream Body { get; set; } + + /// <summary> + /// Gets or sets the value for the <c>Content-Length</c> response header. + /// </summary> + public abstract long? ContentLength { get; set; } + + /// <summary> + /// Gets or sets the value for the <c>Content-Type</c> response header. + /// </summary> + public abstract string ContentType { get; set; } + + /// <summary> + /// Gets an object that can be used to manage cookies for this response. + /// </summary> + public abstract IResponseCookies Cookies { get; } + + /// <summary> + /// Gets a value indicating whether response headers have been sent to the client. + /// </summary> + public abstract bool HasStarted { get; } + + /// <summary> + /// Adds a delegate to be invoked just before response headers will be sent to the client. + /// </summary> + /// <param name="callback">The delegate to execute.</param> + /// <param name="state">A state object to capture and pass back to the delegate.</param> + public abstract void OnStarting(Func<object, Task> callback, object state); + + /// <summary> + /// Adds a delegate to be invoked just before response headers will be sent to the client. + /// </summary> + /// <param name="callback">The delegate to execute.</param> + public virtual void OnStarting(Func<Task> callback) => OnStarting(_callbackDelegate, callback); + + /// <summary> + /// Adds a delegate to be invoked after the response has finished being sent to the client. + /// </summary> + /// <param name="callback">The delegate to invoke.</param> + /// <param name="state">A state object to capture and pass back to the delegate.</param> + public abstract void OnCompleted(Func<object, Task> callback, object state); + + /// <summary> + /// Registers an object for disposal by the host once the request has finished processing. + /// </summary> + /// <param name="disposable">The object to be disposed.</param> + public virtual void RegisterForDispose(IDisposable disposable) => OnCompleted(_disposeDelegate, disposable); + + /// <summary> + /// Adds a delegate to be invoked after the response has finished being sent to the client. + /// </summary> + /// <param name="callback">The delegate to invoke.</param> + public virtual void OnCompleted(Func<Task> callback) => OnCompleted(_callbackDelegate, callback); + + /// <summary> + /// Returns a temporary redirect response (HTTP 302) to the client. + /// </summary> + /// <param name="location">The URL to redirect the client to. This must be properly encoded for use in http headers + /// where only ASCII characters are allowed.</param> + public virtual void Redirect(string location) => Redirect(location, permanent: false); + + /// <summary> + /// Returns a redirect response (HTTP 301 or HTTP 302) to the client. + /// </summary> + /// <param name="location">The URL to redirect the client to. This must be properly encoded for use in http headers + /// where only ASCII characters are allowed.</param> + /// <param name="permanent"><c>True</c> if the redirect is permanent (301), otherwise <c>false</c> (302).</param> + public abstract void Redirect(string location, bool permanent); + } +} diff --git a/src/Http/Http.Abstractions/src/IApplicationBuilder.cs b/src/Http/Http.Abstractions/src/IApplicationBuilder.cs new file mode 100644 index 0000000000000000000000000000000000000000..6110d7f3db6e13327b93ebb0a550c30753ec3765 --- /dev/null +++ b/src/Http/Http.Abstractions/src/IApplicationBuilder.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Builder +{ + /// <summary> + /// Defines a class that provides the mechanisms to configure an application's request pipeline. + /// </summary> + public interface IApplicationBuilder + { + /// <summary> + /// Gets or sets the <see cref="IServiceProvider"/> that provides access to the application's service container. + /// </summary> + IServiceProvider ApplicationServices { get; set; } + + /// <summary> + /// Gets the set of HTTP features the application's server provides. + /// </summary> + IFeatureCollection ServerFeatures { get; } + + /// <summary> + /// Gets a key/value collection that can be used to share data between middleware. + /// </summary> + IDictionary<string, object> Properties { get; } + + /// <summary> + /// Adds a middleware delegate to the application's request pipeline. + /// </summary> + /// <param name="middleware">The middleware delegate.</param> + /// <returns>The <see cref="IApplicationBuilder"/>.</returns> + IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware); + + /// <summary> + /// Creates a new <see cref="IApplicationBuilder"/> that shares the <see cref="Properties"/> of this + /// <see cref="IApplicationBuilder"/>. + /// </summary> + /// <returns>The new <see cref="IApplicationBuilder"/>.</returns> + IApplicationBuilder New(); + + /// <summary> + /// Builds the delegate used by this application to process HTTP requests. + /// </summary> + /// <returns>The request handling delegate.</returns> + RequestDelegate Build(); + } +} diff --git a/src/Http/Http.Abstractions/src/IHttpContextAccessor.cs b/src/Http/Http.Abstractions/src/IHttpContextAccessor.cs new file mode 100644 index 0000000000000000000000000000000000000000..dc8ec34a7c149b6172cfe152d1b015450c8ce6f5 --- /dev/null +++ b/src/Http/Http.Abstractions/src/IHttpContextAccessor.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http +{ + public interface IHttpContextAccessor + { + HttpContext HttpContext { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/IHttpContextFactory.cs b/src/Http/Http.Abstractions/src/IHttpContextFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..7d049626c39aad299767662e6f61045fa1e30f12 --- /dev/null +++ b/src/Http/Http.Abstractions/src/IHttpContextFactory.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http +{ + public interface IHttpContextFactory + { + HttpContext Create(IFeatureCollection featureCollection); + void Dispose(HttpContext httpContext); + } +} diff --git a/src/Http/Http.Abstractions/src/IMiddleware.cs b/src/Http/Http.Abstractions/src/IMiddleware.cs new file mode 100644 index 0000000000000000000000000000000000000000..f92527f3f5e103ad14f1b4ec6400e2fafcc2a9a4 --- /dev/null +++ b/src/Http/Http.Abstractions/src/IMiddleware.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Defines middleware that can be added to the application's request pipeline. + /// </summary> + public interface IMiddleware + { + /// <summary> + /// Request handling method. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> for the current request.</param> + /// <param name="next">The delegate representing the remaining middleware in the request pipeline.</param> + /// <returns>A <see cref="Task"/> that represents the execution of this middleware.</returns> + Task InvokeAsync(HttpContext context, RequestDelegate next); + } +} diff --git a/src/Http/Http.Abstractions/src/IMiddlewareFactory.cs b/src/Http/Http.Abstractions/src/IMiddlewareFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..5d9fda8a751ad3a8ff4cc85281bb7cb3bbfadf19 --- /dev/null +++ b/src/Http/Http.Abstractions/src/IMiddlewareFactory.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Provides methods to create middleware. + /// </summary> + public interface IMiddlewareFactory + { + /// <summary> + /// Creates a middleware instance for each request. + /// </summary> + /// <param name="middlewareType">The concrete <see cref="Type"/> of the <see cref="IMiddleware"/>.</param> + /// <returns>The <see cref="IMiddleware"/> instance.</returns> + IMiddleware Create(Type middlewareType); + + /// <summary> + /// Releases a <see cref="IMiddleware"/> instance at the end of each request. + /// </summary> + /// <param name="middleware">The <see cref="IMiddleware"/> instance to release.</param> + void Release(IMiddleware middleware); + } +} diff --git a/src/Http/Http.Abstractions/src/Internal/HeaderSegment.cs b/src/Http/Http.Abstractions/src/Internal/HeaderSegment.cs new file mode 100644 index 0000000000000000000000000000000000000000..eed9d80f88356f0b4f7468896f97c1fda27bd9dc --- /dev/null +++ b/src/Http/Http.Abstractions/src/Internal/HeaderSegment.cs @@ -0,0 +1,66 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public struct HeaderSegment : IEquatable<HeaderSegment> + { + private readonly StringSegment _formatting; + private readonly StringSegment _data; + + // <summary> + // Initializes a new instance of the <see cref="HeaderSegment"/> structure. + // </summary> + public HeaderSegment(StringSegment formatting, StringSegment data) + { + _formatting = formatting; + _data = data; + } + + public StringSegment Formatting + { + get { return _formatting; } + } + + public StringSegment Data + { + get { return _data; } + } + + public bool Equals(HeaderSegment other) + { + return _formatting.Equals(other._formatting) && _data.Equals(other._data); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is HeaderSegment && Equals((HeaderSegment)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (_formatting.GetHashCode() * 397) ^ _data.GetHashCode(); + } + } + + public static bool operator ==(HeaderSegment left, HeaderSegment right) + { + return left.Equals(right); + } + + public static bool operator !=(HeaderSegment left, HeaderSegment right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Http/Http.Abstractions/src/Internal/HeaderSegmentCollection.cs b/src/Http/Http.Abstractions/src/Internal/HeaderSegmentCollection.cs new file mode 100644 index 0000000000000000000000000000000000000000..40c40a8eb349193f59fb49d328cf375b3cb06cd6 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Internal/HeaderSegmentCollection.cs @@ -0,0 +1,297 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public struct HeaderSegmentCollection : IEnumerable<HeaderSegment>, IEquatable<HeaderSegmentCollection> + { + private readonly StringValues _headers; + + public HeaderSegmentCollection(StringValues headers) + { + _headers = headers; + } + + public bool Equals(HeaderSegmentCollection other) + { + return StringValues.Equals(_headers, other._headers); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is HeaderSegmentCollection && Equals((HeaderSegmentCollection)obj); + } + + public override int GetHashCode() + { + return (!StringValues.IsNullOrEmpty(_headers) ? _headers.GetHashCode() : 0); + } + + public static bool operator ==(HeaderSegmentCollection left, HeaderSegmentCollection right) + { + return left.Equals(right); + } + + public static bool operator !=(HeaderSegmentCollection left, HeaderSegmentCollection right) + { + return !left.Equals(right); + } + + public Enumerator GetEnumerator() + { + return new Enumerator(_headers); + } + + IEnumerator<HeaderSegment> IEnumerable<HeaderSegment>.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator<HeaderSegment> + { + private readonly StringValues _headers; + private int _index; + + private string _header; + private int _headerLength; + private int _offset; + + private int _leadingStart; + private int _leadingEnd; + private int _valueStart; + private int _valueEnd; + private int _trailingStart; + + private Mode _mode; + + public Enumerator(StringValues headers) + { + _headers = headers; + _header = string.Empty; + _headerLength = -1; + _index = -1; + _offset = -1; + _leadingStart = -1; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + _mode = Mode.Leading; + } + + private enum Mode + { + Leading, + Value, + ValueQuoted, + Trailing, + Produce, + } + + private enum Attr + { + Value, + Quote, + Delimiter, + Whitespace + } + + public HeaderSegment Current + { + get + { + return new HeaderSegment( + new StringSegment(_header, _leadingStart, _leadingEnd - _leadingStart), + new StringSegment(_header, _valueStart, _valueEnd - _valueStart)); + } + } + + object IEnumerator.Current + { + get { return Current; } + } + + public void Dispose() + { + } + + public bool MoveNext() + { + while (true) + { + if (_mode == Mode.Produce) + { + _leadingStart = _trailingStart; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + + if (_offset == _headerLength && + _leadingStart != -1 && + _leadingStart != _offset) + { + // Also produce trailing whitespace + _leadingEnd = _offset; + return true; + } + _mode = Mode.Leading; + } + + // if end of a string + if (_offset == _headerLength) + { + ++_index; + _offset = -1; + _leadingStart = 0; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + + // if that was the last string + if (_index == _headers.Count) + { + // no more move nexts + return false; + } + + // grab the next string + _header = _headers[_index] ?? string.Empty; + _headerLength = _header.Length; + } + while (true) + { + ++_offset; + char ch = _offset == _headerLength ? (char)0 : _header[_offset]; + // todo - array of attrs + Attr attr = char.IsWhiteSpace(ch) ? Attr.Whitespace : ch == '\"' ? Attr.Quote : (ch == ',' || ch == (char)0) ? Attr.Delimiter : Attr.Value; + + switch (_mode) + { + case Mode.Leading: + switch (attr) + { + case Attr.Delimiter: + _valueStart = _valueStart == -1 ? _offset : _valueStart; + _valueEnd = _valueEnd == -1 ? _offset : _valueEnd; + _trailingStart = _trailingStart == -1 ? _offset : _trailingStart; + _leadingEnd = _offset; + _mode = Mode.Produce; + break; + case Attr.Quote: + _leadingEnd = _offset; + _valueStart = _offset; + _mode = Mode.ValueQuoted; + break; + case Attr.Value: + _leadingEnd = _offset; + _valueStart = _offset; + _mode = Mode.Value; + break; + case Attr.Whitespace: + // more + break; + } + break; + case Mode.Value: + switch (attr) + { + case Attr.Quote: + _mode = Mode.ValueQuoted; + break; + case Attr.Delimiter: + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Produce; + break; + case Attr.Value: + // more + break; + case Attr.Whitespace: + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Trailing; + break; + } + break; + case Mode.ValueQuoted: + switch (attr) + { + case Attr.Quote: + _mode = Mode.Value; + break; + case Attr.Delimiter: + if (ch == (char)0) + { + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Produce; + } + break; + case Attr.Value: + case Attr.Whitespace: + // more + break; + } + break; + case Mode.Trailing: + switch (attr) + { + case Attr.Delimiter: + _mode = Mode.Produce; + break; + case Attr.Quote: + // back into value + _trailingStart = -1; + _valueEnd = -1; + _mode = Mode.ValueQuoted; + break; + case Attr.Value: + // back into value + _trailingStart = -1; + _valueEnd = -1; + _mode = Mode.Value; + break; + case Attr.Whitespace: + // more + break; + } + break; + } + if (_mode == Mode.Produce) + { + return true; + } + } + } + } + + public void Reset() + { + _index = 0; + _offset = 0; + _leadingStart = 0; + _leadingEnd = 0; + _valueStart = 0; + _valueEnd = 0; + } + } + } + +} diff --git a/src/Http/Http.Abstractions/src/Internal/HostStringHelper.cs b/src/Http/Http.Abstractions/src/Internal/HostStringHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..f4cfac52afb3c0c91ced09ccceef0ce450e343ff --- /dev/null +++ b/src/Http/Http.Abstractions/src/Internal/HostStringHelper.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Internal +{ + internal class HostStringHelper + { + // Allowed Characters: + // A-Z, a-z, 0-9, ., + // -, %, [, ], : + // Above for IPV6 + private static bool[] SafeHostStringChars = { + false, false, false, false, false, false, false, false, // 0x00 - 0x07 + false, false, false, false, false, false, false, false, // 0x08 - 0x0F + false, false, false, false, false, false, false, false, // 0x10 - 0x17 + false, false, false, false, false, false, false, false, // 0x18 - 0x1F + false, false, false, false, false, true, false, false, // 0x20 - 0x27 + false, false, false, false, false, true, true, false, // 0x28 - 0x2F + true, true, true, true, true, true, true, true, // 0x30 - 0x37 + true, true, true, false, false, false, false, false, // 0x38 - 0x3F + false, true, true, true, true, true, true, true, // 0x40 - 0x47 + true, true, true, true, true, true, true, true, // 0x48 - 0x4F + true, true, true, true, true, true, true, true, // 0x50 - 0x57 + true, true, true, true, false, true, false, false, // 0x58 - 0x5F + false, true, true, true, true, true, true, true, // 0x60 - 0x67 + true, true, true, true, true, true, true, true, // 0x68 - 0x6F + true, true, true, true, true, true, true, true, // 0x70 - 0x77 + true, true, true, false, false, false, false, false, // 0x78 - 0x7F + }; + + public static bool IsSafeHostStringChar(char c) + { + return c < SafeHostStringChars.Length && SafeHostStringChars[c]; + } + } +} diff --git a/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs b/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs new file mode 100644 index 0000000000000000000000000000000000000000..185fc40ac7c76b01397007de698f4b74a3790c90 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs @@ -0,0 +1,165 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public static class ParsingHelpers + { + public static StringValues GetHeader(IHeaderDictionary headers, string key) + { + StringValues value; + return headers.TryGetValue(key, out value) ? value : StringValues.Empty; + } + + public static StringValues GetHeaderSplit(IHeaderDictionary headers, string key) + { + var values = GetHeaderUnmodified(headers, key); + return new StringValues(GetHeaderSplitImplementation(values).ToArray()); + } + + private static IEnumerable<string> GetHeaderSplitImplementation(StringValues values) + { + foreach (var segment in new HeaderSegmentCollection(values)) + { + if (!StringSegment.IsNullOrEmpty(segment.Data)) + { + var value = DeQuote(segment.Data.Value); + if (!string.IsNullOrEmpty(value)) + { + yield return value; + } + } + } + } + + public static StringValues GetHeaderUnmodified(IHeaderDictionary headers, string key) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + StringValues values; + return headers.TryGetValue(key, out values) ? values : StringValues.Empty; + } + + public static void SetHeaderJoined(IHeaderDictionary headers, string key, StringValues value) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException(nameof(key)); + } + if (StringValues.IsNullOrEmpty(value)) + { + headers.Remove(key); + } + else + { + headers[key] = string.Join(",", value.Select((s) => QuoteIfNeeded(s))); + } + } + + // Quote items that contain commas and are not already quoted. + private static string QuoteIfNeeded(string value) + { + if (!string.IsNullOrEmpty(value) && + value.Contains(',') && + (value[0] != '"' || value[value.Length - 1] != '"')) + { + return $"\"{value}\""; + } + return value; + } + + private static string DeQuote(string value) + { + if (!string.IsNullOrEmpty(value) && + (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"')) + { + value = value.Substring(1, value.Length - 2); + } + + return value; + } + + public static void SetHeaderUnmodified(IHeaderDictionary headers, string key, StringValues? values) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException(nameof(key)); + } + if (!values.HasValue || StringValues.IsNullOrEmpty(values.Value)) + { + headers.Remove(key); + } + else + { + headers[key] = values.Value; + } + } + + public static void AppendHeaderJoined(IHeaderDictionary headers, string key, params string[] values) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (values == null || values.Length == 0) + { + return; + } + + string existing = GetHeader(headers, key); + if (existing == null) + { + SetHeaderJoined(headers, key, values); + } + else + { + headers[key] = existing + "," + string.Join(",", values.Select(value => QuoteIfNeeded(value))); + } + } + + public static void AppendHeaderUnmodified(IHeaderDictionary headers, string key, StringValues values) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (values.Count == 0) + { + return; + } + + var existing = GetHeaderUnmodified(headers, key); + SetHeaderUnmodified(headers, key, StringValues.Concat(existing, values)); + } + } +} diff --git a/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs b/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..b6cebb2b9c99cbdff5ef8822d453c5bacffe937b --- /dev/null +++ b/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Internal +{ + internal class PathStringHelper + { + private static bool[] ValidPathChars = { + false, false, false, false, false, false, false, false, // 0x00 - 0x07 + false, false, false, false, false, false, false, false, // 0x08 - 0x0F + false, false, false, false, false, false, false, false, // 0x10 - 0x17 + false, false, false, false, false, false, false, false, // 0x18 - 0x1F + false, true, false, false, true, false, true, true, // 0x20 - 0x27 + true, true, true, true, true, true, true, true, // 0x28 - 0x2F + true, true, true, true, true, true, true, true, // 0x30 - 0x37 + true, true, true, true, false, true, false, false, // 0x38 - 0x3F + true, true, true, true, true, true, true, true, // 0x40 - 0x47 + true, true, true, true, true, true, true, true, // 0x48 - 0x4F + true, true, true, true, true, true, true, true, // 0x50 - 0x57 + true, true, true, false, false, false, false, true, // 0x58 - 0x5F + false, true, true, true, true, true, true, true, // 0x60 - 0x67 + true, true, true, true, true, true, true, true, // 0x68 - 0x6F + true, true, true, true, true, true, true, true, // 0x70 - 0x77 + true, true, true, false, false, false, true, false, // 0x78 - 0x7F + }; + + public static bool IsValidPathChar(char c) + { + return c < ValidPathChars.Length && ValidPathChars[c]; + } + + public static bool IsPercentEncodedChar(string str, int index) + { + return index < str.Length - 2 + && str[index] == '%' + && IsHexadecimalChar(str[index + 1]) + && IsHexadecimalChar(str[index + 2]); + } + + public static bool IsHexadecimalChar(char c) + { + return ('0' <= c && c <= '9') + || ('A' <= c && c <= 'F') + || ('a' <= c && c <= 'f'); + } + } +} diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj new file mode 100644 index 0000000000000000000000000000000000000000..821b40cb191fcd70965f7df4623a8353ee1ef254 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -0,0 +1,23 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + + <Description>ASP.NET Core HTTP object model for HTTP requests and responses and also common extension methods for registering middleware in an IApplicationBuilder. +Commonly used types: +Microsoft.AspNetCore.Builder.IApplicationBuilder +Microsoft.AspNetCore.Http.HttpContext +Microsoft.AspNetCore.Http.HttpRequest +Microsoft.AspNetCore.Http.HttpResponse</Description> + <TargetFramework>netstandard2.0</TargetFramework> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore</PackageTags> + <NoWarn>$(NoWarn);CS1591</NoWarn> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Http.Features" /> + <Reference Include="Microsoft.Extensions.ActivatorUtilities.Sources" PrivateAssets="All" /> + <Reference Include="System.Text.Encodings.Web" /> + </ItemGroup> + +</Project> diff --git a/src/Http/Http.Abstractions/src/PathString.cs b/src/Http/Http.Abstractions/src/PathString.cs new file mode 100644 index 0000000000000000000000000000000000000000..2a5960b661112bce3fb5544e03c1f20357f80872 --- /dev/null +++ b/src/Http/Http.Abstractions/src/PathString.cs @@ -0,0 +1,477 @@ +// 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.ComponentModel; +using System.Globalization; +using System.Text; +using Microsoft.AspNetCore.Http.Abstractions; +using Microsoft.AspNetCore.Http.Internal; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Provides correct escaping for Path and PathBase values when needed to reconstruct a request or redirect URI string + /// </summary> + [TypeConverter(typeof(PathStringConverter))] + public struct PathString : IEquatable<PathString> + { + private static readonly char[] splitChar = { '/' }; + + /// <summary> + /// Represents the empty path. This field is read-only. + /// </summary> + public static readonly PathString Empty = new PathString(string.Empty); + + private readonly string _value; + + /// <summary> + /// Initalize the path string with a given value. This value must be in unescaped format. Use + /// PathString.FromUriComponent(value) if you have a path value which is in an escaped format. + /// </summary> + /// <param name="value">The unescaped path to be assigned to the Value property.</param> + public PathString(string value) + { + if (!string.IsNullOrEmpty(value) && value[0] != '/') + { + throw new ArgumentException(Resources.FormatException_PathMustStartWithSlash(nameof(value)), nameof(value)); + } + _value = value; + } + + /// <summary> + /// The unescaped path value + /// </summary> + public string Value + { + get { return _value; } + } + + /// <summary> + /// True if the path is not empty + /// </summary> + public bool HasValue + { + get { return !string.IsNullOrEmpty(_value); } + } + + /// <summary> + /// Provides the path string escaped in a way which is correct for combining into the URI representation. + /// </summary> + /// <returns>The escaped path value</returns> + public override string ToString() + { + return ToUriComponent(); + } + + /// <summary> + /// Provides the path string escaped in a way which is correct for combining into the URI representation. + /// </summary> + /// <returns>The escaped path value</returns> + public string ToUriComponent() + { + if (!HasValue) + { + return string.Empty; + } + + StringBuilder buffer = null; + + var start = 0; + var count = 0; + var requiresEscaping = false; + var i = 0; + + while (i < _value.Length) + { + var isPercentEncodedChar = PathStringHelper.IsPercentEncodedChar(_value, i); + if (PathStringHelper.IsValidPathChar(_value[i]) || isPercentEncodedChar) + { + if (requiresEscaping) + { + // the current segment requires escape + if (buffer == null) + { + buffer = new StringBuilder(_value.Length * 3); + } + + buffer.Append(Uri.EscapeDataString(_value.Substring(start, count))); + + requiresEscaping = false; + start = i; + count = 0; + } + + if (isPercentEncodedChar) + { + count += 3; + i += 3; + } + else + { + count++; + i++; + } + } + else + { + if (!requiresEscaping) + { + // the current segument doesn't require escape + if (buffer == null) + { + buffer = new StringBuilder(_value.Length * 3); + } + + buffer.Append(_value, start, count); + + requiresEscaping = true; + start = i; + count = 0; + } + + count++; + i++; + } + } + + if (count == _value.Length && !requiresEscaping) + { + return _value; + } + else + { + if (count > 0) + { + if (buffer == null) + { + buffer = new StringBuilder(_value.Length * 3); + } + + if (requiresEscaping) + { + buffer.Append(Uri.EscapeDataString(_value.Substring(start, count))); + } + else + { + buffer.Append(_value, start, count); + } + } + + return buffer.ToString(); + } + } + + + /// <summary> + /// Returns an PathString given the path as it is escaped in the URI format. The string MUST NOT contain any + /// value that is not a path. + /// </summary> + /// <param name="uriComponent">The escaped path as it appears in the URI format.</param> + /// <returns>The resulting PathString</returns> + public static PathString FromUriComponent(string uriComponent) + { + // REVIEW: what is the exactly correct thing to do? + return new PathString(Uri.UnescapeDataString(uriComponent)); + } + + /// <summary> + /// Returns an PathString given the path as from a Uri object. Relative Uri objects are not supported. + /// </summary> + /// <param name="uri">The Uri object</param> + /// <returns>The resulting PathString</returns> + public static PathString FromUriComponent(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + // REVIEW: what is the exactly correct thing to do? + return new PathString("/" + uri.GetComponents(UriComponents.Path, UriFormat.Unescaped)); + } + + /// <summary> + /// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/>. + /// </summary> + /// <param name="other">The <see cref="PathString"/> to compare.</param> + /// <returns>true if value matches the beginning of this string; otherwise, false.</returns> + public bool StartsWithSegments(PathString other) + { + return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase); + } + + /// <summary> + /// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> when compared + /// using the specified comparison option. + /// </summary> + /// <param name="other">The <see cref="PathString"/> to compare.</param> + /// <param name="comparisonType">One of the enumeration values that determines how this <see cref="PathString"/> and value are compared.</param> + /// <returns>true if value matches the beginning of this string; otherwise, false.</returns> + public bool StartsWithSegments(PathString other, StringComparison comparisonType) + { + var value1 = Value ?? string.Empty; + var value2 = other.Value ?? string.Empty; + if (value1.StartsWith(value2, comparisonType)) + { + return value1.Length == value2.Length || value1[value2.Length] == '/'; + } + return false; + } + + /// <summary> + /// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> and returns + /// the remaining segments. + /// </summary> + /// <param name="other">The <see cref="PathString"/> to compare.</param> + /// <param name="remaining">The remaining segments after the match.</param> + /// <returns>true if value matches the beginning of this string; otherwise, false.</returns> + public bool StartsWithSegments(PathString other, out PathString remaining) + { + return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out remaining); + } + + /// <summary> + /// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> when compared + /// using the specified comparison option and returns the remaining segments. + /// </summary> + /// <param name="other">The <see cref="PathString"/> to compare.</param> + /// <param name="comparisonType">One of the enumeration values that determines how this <see cref="PathString"/> and value are compared.</param> + /// <param name="remaining">The remaining segments after the match.</param> + /// <returns>true if value matches the beginning of this string; otherwise, false.</returns> + public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString remaining) + { + var value1 = Value ?? string.Empty; + var value2 = other.Value ?? string.Empty; + if (value1.StartsWith(value2, comparisonType)) + { + if (value1.Length == value2.Length || value1[value2.Length] == '/') + { + remaining = new PathString(value1.Substring(value2.Length)); + return true; + } + } + remaining = Empty; + return false; + } + + /// <summary> + /// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> and returns + /// the matched and remaining segments. + /// </summary> + /// <param name="other">The <see cref="PathString"/> to compare.</param> + /// <param name="matched">The matched segments with the original casing in the source value.</param> + /// <param name="remaining">The remaining segments after the match.</param> + /// <returns>true if value matches the beginning of this string; otherwise, false.</returns> + public bool StartsWithSegments(PathString other, out PathString matched, out PathString remaining) + { + return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out matched, out remaining); + } + + /// <summary> + /// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> when compared + /// using the specified comparison option and returns the matched and remaining segments. + /// </summary> + /// <param name="other">The <see cref="PathString"/> to compare.</param> + /// <param name="comparisonType">One of the enumeration values that determines how this <see cref="PathString"/> and value are compared.</param> + /// <param name="matched">The matched segments with the original casing in the source value.</param> + /// <param name="remaining">The remaining segments after the match.</param> + /// <returns>true if value matches the beginning of this string; otherwise, false.</returns> + public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString matched, out PathString remaining) + { + var value1 = Value ?? string.Empty; + var value2 = other.Value ?? string.Empty; + if (value1.StartsWith(value2, comparisonType)) + { + if (value1.Length == value2.Length || value1[value2.Length] == '/') + { + matched = new PathString(value1.Substring(0, value2.Length)); + remaining = new PathString(value1.Substring(value2.Length)); + return true; + } + } + remaining = Empty; + matched = Empty; + return false; + } + + /// <summary> + /// Adds two PathString instances into a combined PathString value. + /// </summary> + /// <returns>The combined PathString value</returns> + public PathString Add(PathString other) + { + if (HasValue && + other.HasValue && + Value[Value.Length - 1] == '/') + { + // If the path string has a trailing slash and the other string has a leading slash, we need + // to trim one of them. + return new PathString(Value + other.Value.Substring(1)); + } + + return new PathString(Value + other.Value); + } + + /// <summary> + /// Combines a PathString and QueryString into the joined URI formatted string value. + /// </summary> + /// <returns>The joined URI formatted string value</returns> + public string Add(QueryString other) + { + return ToUriComponent() + other.ToUriComponent(); + } + + /// <summary> + /// Compares this PathString value to another value. The default comparison is StringComparison.OrdinalIgnoreCase. + /// </summary> + /// <param name="other">The second PathString for comparison.</param> + /// <returns>True if both PathString values are equal</returns> + public bool Equals(PathString other) + { + return Equals(other, StringComparison.OrdinalIgnoreCase); + } + + /// <summary> + /// Compares this PathString value to another value using a specific StringComparison type + /// </summary> + /// <param name="other">The second PathString for comparison</param> + /// <param name="comparisonType">The StringComparison type to use</param> + /// <returns>True if both PathString values are equal</returns> + public bool Equals(PathString other, StringComparison comparisonType) + { + if (!HasValue && !other.HasValue) + { + return true; + } + return string.Equals(_value, other._value, comparisonType); + } + + /// <summary> + /// Compares this PathString value to another value. The default comparison is StringComparison.OrdinalIgnoreCase. + /// </summary> + /// <param name="obj">The second PathString for comparison.</param> + /// <returns>True if both PathString values are equal</returns> + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return !HasValue; + } + return obj is PathString && Equals((PathString)obj); + } + + /// <summary> + /// Returns the hash code for the PathString value. The hash code is provided by the OrdinalIgnoreCase implementation. + /// </summary> + /// <returns>The hash code</returns> + public override int GetHashCode() + { + return (HasValue ? StringComparer.OrdinalIgnoreCase.GetHashCode(_value) : 0); + } + + /// <summary> + /// Operator call through to Equals + /// </summary> + /// <param name="left">The left parameter</param> + /// <param name="right">The right parameter</param> + /// <returns>True if both PathString values are equal</returns> + public static bool operator ==(PathString left, PathString right) + { + return left.Equals(right); + } + + /// <summary> + /// Operator call through to Equals + /// </summary> + /// <param name="left">The left parameter</param> + /// <param name="right">The right parameter</param> + /// <returns>True if both PathString values are not equal</returns> + public static bool operator !=(PathString left, PathString right) + { + return !left.Equals(right); + } + + /// <summary> + /// </summary> + /// <param name="left">The left parameter</param> + /// <param name="right">The right parameter</param> + /// <returns>The ToString combination of both values</returns> + public static string operator +(string left, PathString right) + { + // This overload exists to prevent the implicit string<->PathString converter from + // trying to call the PathString+PathString operator for things that are not path strings. + return string.Concat(left, right.ToString()); + } + + /// <summary> + /// </summary> + /// <param name="left">The left parameter</param> + /// <param name="right">The right parameter</param> + /// <returns>The ToString combination of both values</returns> + public static string operator +(PathString left, string right) + { + // This overload exists to prevent the implicit string<->PathString converter from + // trying to call the PathString+PathString operator for things that are not path strings. + return string.Concat(left.ToString(), right); + } + + /// <summary> + /// Operator call through to Add + /// </summary> + /// <param name="left">The left parameter</param> + /// <param name="right">The right parameter</param> + /// <returns>The PathString combination of both values</returns> + public static PathString operator +(PathString left, PathString right) + { + return left.Add(right); + } + + /// <summary> + /// Operator call through to Add + /// </summary> + /// <param name="left">The left parameter</param> + /// <param name="right">The right parameter</param> + /// <returns>The PathString combination of both values</returns> + public static string operator +(PathString left, QueryString right) + { + return left.Add(right); + } + + /// <summary> + /// Implicitly creates a new PathString from the given string. + /// </summary> + /// <param name="s"></param> + public static implicit operator PathString(string s) + => ConvertFromString(s); + + /// <summary> + /// Implicitly calls ToString(). + /// </summary> + /// <param name="path"></param> + public static implicit operator string(PathString path) + => path.ToString(); + + internal static PathString ConvertFromString(string s) + => string.IsNullOrEmpty(s) ? new PathString(s) : FromUriComponent(s); + } + + internal class PathStringConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + => sourceType == typeof(string) + ? true + : base.CanConvertFrom(context, sourceType); + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + => value is string + ? PathString.ConvertFromString((string)value) + : base.ConvertFrom(context, culture, value); + + public override object ConvertTo(ITypeDescriptorContext context, + CultureInfo culture, object value, Type destinationType) + => destinationType == typeof(string) + ? value.ToString() + : base.ConvertTo(context, culture, value, destinationType); + } +} diff --git a/src/Http/Http.Abstractions/src/Properties/AssemblyInfo.cs b/src/Http/Http.Abstractions/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..2bdc2a912f1891322c34311475d6a511dd24e5b1 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Http.Abstractions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Properties/Resources.Designer.cs b/src/Http/Http.Abstractions/src/Properties/Resources.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..6af7d138bee645559f6c98b50eb1900a2ad9ba66 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Properties/Resources.Designer.cs @@ -0,0 +1,212 @@ +// <auto-generated /> +namespace Microsoft.AspNetCore.Http.Abstractions +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Http.Abstractions.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// <summary> + /// '{0}' is not available. + /// </summary> + internal static string Exception_UseMiddlewareIServiceProviderNotAvailable + { + get => GetString("Exception_UseMiddlewareIServiceProviderNotAvailable"); + } + + /// <summary> + /// '{0}' is not available. + /// </summary> + internal static string FormatException_UseMiddlewareIServiceProviderNotAvailable(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddlewareIServiceProviderNotAvailable"), p0); + + /// <summary> + /// No public '{0}' or '{1}' method found for middleware of type '{2}'. + /// </summary> + internal static string Exception_UseMiddlewareNoInvokeMethod + { + get => GetString("Exception_UseMiddlewareNoInvokeMethod"); + } + + /// <summary> + /// No public '{0}' or '{1}' method found for middleware of type '{2}'. + /// </summary> + internal static string FormatException_UseMiddlewareNoInvokeMethod(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddlewareNoInvokeMethod"), p0, p1, p2); + + /// <summary> + /// '{0}' or '{1}' does not return an object of type '{2}'. + /// </summary> + internal static string Exception_UseMiddlewareNonTaskReturnType + { + get => GetString("Exception_UseMiddlewareNonTaskReturnType"); + } + + /// <summary> + /// '{0}' or '{1}' does not return an object of type '{2}'. + /// </summary> + internal static string FormatException_UseMiddlewareNonTaskReturnType(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddlewareNonTaskReturnType"), p0, p1, p2); + + /// <summary> + /// The '{0}' or '{1}' method's first argument must be of type '{2}'. + /// </summary> + internal static string Exception_UseMiddlewareNoParameters + { + get => GetString("Exception_UseMiddlewareNoParameters"); + } + + /// <summary> + /// The '{0}' or '{1}' method's first argument must be of type '{2}'. + /// </summary> + internal static string FormatException_UseMiddlewareNoParameters(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddlewareNoParameters"), p0, p1, p2); + + /// <summary> + /// Multiple public '{0}' or '{1}' methods are available. + /// </summary> + internal static string Exception_UseMiddleMutlipleInvokes + { + get => GetString("Exception_UseMiddleMutlipleInvokes"); + } + + /// <summary> + /// Multiple public '{0}' or '{1}' methods are available. + /// </summary> + internal static string FormatException_UseMiddleMutlipleInvokes(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddleMutlipleInvokes"), p0, p1); + + /// <summary> + /// The path in '{0}' must start with '/'. + /// </summary> + internal static string Exception_PathMustStartWithSlash + { + get => GetString("Exception_PathMustStartWithSlash"); + } + + /// <summary> + /// The path in '{0}' must start with '/'. + /// </summary> + internal static string FormatException_PathMustStartWithSlash(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_PathMustStartWithSlash"), p0); + + /// <summary> + /// Unable to resolve service for type '{0}' while attempting to Invoke middleware '{1}'. + /// </summary> + internal static string Exception_InvokeMiddlewareNoService + { + get => GetString("Exception_InvokeMiddlewareNoService"); + } + + /// <summary> + /// Unable to resolve service for type '{0}' while attempting to Invoke middleware '{1}'. + /// </summary> + internal static string FormatException_InvokeMiddlewareNoService(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_InvokeMiddlewareNoService"), p0, p1); + + /// <summary> + /// The '{0}' method must not have ref or out parameters. + /// </summary> + internal static string Exception_InvokeDoesNotSupportRefOrOutParams + { + get => GetString("Exception_InvokeDoesNotSupportRefOrOutParams"); + } + + /// <summary> + /// The '{0}' method must not have ref or out parameters. + /// </summary> + internal static string FormatException_InvokeDoesNotSupportRefOrOutParams(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_InvokeDoesNotSupportRefOrOutParams"), p0); + + /// <summary> + /// The value must be greater than zero. + /// </summary> + internal static string Exception_PortMustBeGreaterThanZero + { + get => GetString("Exception_PortMustBeGreaterThanZero"); + } + + /// <summary> + /// The value must be greater than zero. + /// </summary> + internal static string FormatException_PortMustBeGreaterThanZero() + => GetString("Exception_PortMustBeGreaterThanZero"); + + /// <summary> + /// No service for type '{0}' has been registered. + /// </summary> + internal static string Exception_UseMiddlewareNoMiddlewareFactory + { + get => GetString("Exception_UseMiddlewareNoMiddlewareFactory"); + } + + /// <summary> + /// No service for type '{0}' has been registered. + /// </summary> + internal static string FormatException_UseMiddlewareNoMiddlewareFactory(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddlewareNoMiddlewareFactory"), p0); + + /// <summary> + /// '{0}' failed to create middleware of type '{1}'. + /// </summary> + internal static string Exception_UseMiddlewareUnableToCreateMiddleware + { + get => GetString("Exception_UseMiddlewareUnableToCreateMiddleware"); + } + + /// <summary> + /// '{0}' failed to create middleware of type '{1}'. + /// </summary> + internal static string FormatException_UseMiddlewareUnableToCreateMiddleware(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddlewareUnableToCreateMiddleware"), p0, p1); + + /// <summary> + /// Types that implement '{0}' do not support explicit arguments. + /// </summary> + internal static string Exception_UseMiddlewareExplicitArgumentsNotSupported + { + get => GetString("Exception_UseMiddlewareExplicitArgumentsNotSupported"); + } + + /// <summary> + /// Types that implement '{0}' do not support explicit arguments. + /// </summary> + internal static string FormatException_UseMiddlewareExplicitArgumentsNotSupported(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddlewareExplicitArgumentsNotSupported"), p0); + + /// <summary> + /// Argument cannot be null or empty. + /// </summary> + internal static string ArgumentCannotBeNullOrEmpty + { + get => GetString("ArgumentCannotBeNullOrEmpty"); + } + + /// <summary> + /// Argument cannot be null or empty. + /// </summary> + internal static string FormatArgumentCannotBeNullOrEmpty() + => GetString("ArgumentCannotBeNullOrEmpty"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Http/Http.Abstractions/src/QueryString.cs b/src/Http/Http.Abstractions/src/QueryString.cs new file mode 100644 index 0000000000000000000000000000000000000000..772df8dfd9b7dbe2f4e00d1a13878aa05a5b5339 --- /dev/null +++ b/src/Http/Http.Abstractions/src/QueryString.cs @@ -0,0 +1,261 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Encodings.Web; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Provides correct handling for QueryString value when needed to reconstruct a request or redirect URI string + /// </summary> + public struct QueryString : IEquatable<QueryString> + { + /// <summary> + /// Represents the empty query string. This field is read-only. + /// </summary> + public static readonly QueryString Empty = new QueryString(string.Empty); + + private readonly string _value; + + /// <summary> + /// Initialize the query string with a given value. This value must be in escaped and delimited format with + /// a leading '?' character. + /// </summary> + /// <param name="value">The query string to be assigned to the Value property.</param> + public QueryString(string value) + { + if (!string.IsNullOrEmpty(value) && value[0] != '?') + { + throw new ArgumentException("The leading '?' must be included for a non-empty query.", nameof(value)); + } + _value = value; + } + + /// <summary> + /// The escaped query string with the leading '?' character + /// </summary> + public string Value + { + get { return _value; } + } + + /// <summary> + /// True if the query string is not empty + /// </summary> + public bool HasValue + { + get { return !string.IsNullOrEmpty(_value); } + } + + /// <summary> + /// Provides the query string escaped in a way which is correct for combining into the URI representation. + /// A leading '?' character will be included unless the Value is null or empty. Characters which are potentially + /// dangerous are escaped. + /// </summary> + /// <returns>The query string value</returns> + public override string ToString() + { + return ToUriComponent(); + } + + /// <summary> + /// Provides the query string escaped in a way which is correct for combining into the URI representation. + /// A leading '?' character will be included unless the Value is null or empty. Characters which are potentially + /// dangerous are escaped. + /// </summary> + /// <returns>The query string value</returns> + public string ToUriComponent() + { + // Escape things properly so System.Uri doesn't mis-interpret the data. + return HasValue ? _value.Replace("#", "%23") : string.Empty; + } + + /// <summary> + /// Returns an QueryString given the query as it is escaped in the URI format. The string MUST NOT contain any + /// value that is not a query. + /// </summary> + /// <param name="uriComponent">The escaped query as it appears in the URI format.</param> + /// <returns>The resulting QueryString</returns> + public static QueryString FromUriComponent(string uriComponent) + { + if (string.IsNullOrEmpty(uriComponent)) + { + return new QueryString(string.Empty); + } + return new QueryString(uriComponent); + } + + /// <summary> + /// Returns an QueryString given the query as from a Uri object. Relative Uri objects are not supported. + /// </summary> + /// <param name="uri">The Uri object</param> + /// <returns>The resulting QueryString</returns> + public static QueryString FromUriComponent(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + string queryValue = uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped); + if (!string.IsNullOrEmpty(queryValue)) + { + queryValue = "?" + queryValue; + } + return new QueryString(queryValue); + } + + /// <summary> + /// Create a query string with a single given parameter name and value. + /// </summary> + /// <param name="name">The un-encoded parameter name</param> + /// <param name="value">The un-encoded parameter value</param> + /// <returns>The resulting QueryString</returns> + public static QueryString Create(string name, string value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (!string.IsNullOrEmpty(value)) + { + value = UrlEncoder.Default.Encode(value); + } + return new QueryString($"?{UrlEncoder.Default.Encode(name)}={value}"); + } + + /// <summary> + /// Creates a query string composed from the given name value pairs. + /// </summary> + /// <param name="parameters"></param> + /// <returns>The resulting QueryString</returns> + public static QueryString Create(IEnumerable<KeyValuePair<string, string>> parameters) + { + var builder = new StringBuilder(); + bool first = true; + foreach (var pair in parameters) + { + AppendKeyValuePair(builder, pair.Key, pair.Value, first); + first = false; + } + + return new QueryString(builder.ToString()); + } + + /// <summary> + /// Creates a query string composed from the given name value pairs. + /// </summary> + /// <param name="parameters"></param> + /// <returns>The resulting QueryString</returns> + public static QueryString Create(IEnumerable<KeyValuePair<string, StringValues>> parameters) + { + var builder = new StringBuilder(); + bool first = true; + + foreach (var pair in parameters) + { + // If nothing in this pair.Values, append null value and continue + if (StringValues.IsNullOrEmpty(pair.Value)) + { + AppendKeyValuePair(builder, pair.Key, null, first); + first = false; + continue; + } + // Otherwise, loop through values in pair.Value + foreach (var value in pair.Value) + { + AppendKeyValuePair(builder, pair.Key, value, first); + first = false; + } + } + + return new QueryString(builder.ToString()); + } + + public QueryString Add(QueryString other) + { + if (!HasValue || Value.Equals("?", StringComparison.Ordinal)) + { + return other; + } + if (!other.HasValue || other.Value.Equals("?", StringComparison.Ordinal)) + { + return this; + } + + // ?name1=value1 Add ?name2=value2 returns ?name1=value1&name2=value2 + return new QueryString(_value + "&" + other.Value.Substring(1)); + } + + public QueryString Add(string name, string value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (!HasValue || Value.Equals("?", StringComparison.Ordinal)) + { + return Create(name, value); + } + + var builder = new StringBuilder(Value); + AppendKeyValuePair(builder, name, value, first: false); + return new QueryString(builder.ToString()); + } + + public bool Equals(QueryString other) + { + if (!HasValue && !other.HasValue) + { + return true; + } + return string.Equals(_value, other._value, StringComparison.Ordinal); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return !HasValue; + } + return obj is QueryString && Equals((QueryString)obj); + } + + public override int GetHashCode() + { + return (HasValue ? _value.GetHashCode() : 0); + } + + public static bool operator ==(QueryString left, QueryString right) + { + return left.Equals(right); + } + + public static bool operator !=(QueryString left, QueryString right) + { + return !left.Equals(right); + } + + public static QueryString operator +(QueryString left, QueryString right) + { + return left.Add(right); + } + + private static void AppendKeyValuePair(StringBuilder builder, string key, string value, bool first) + { + builder.Append(first ? "?" : "&"); + builder.Append(UrlEncoder.Default.Encode(key)); + builder.Append("="); + if (!string.IsNullOrEmpty(value)) + { + builder.Append(UrlEncoder.Default.Encode(value)); + } + } + } +} diff --git a/src/Http/Http.Abstractions/src/RequestDelegate.cs b/src/Http/Http.Abstractions/src/RequestDelegate.cs new file mode 100644 index 0000000000000000000000000000000000000000..aecf353b29d8bbfb337f1359991cb920865a2973 --- /dev/null +++ b/src/Http/Http.Abstractions/src/RequestDelegate.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// A function that can process an HTTP request. + /// </summary> + /// <param name="context">The <see cref="HttpContext"/> for the request.</param> + /// <returns>A task that represents the completion of request processing.</returns> + public delegate Task RequestDelegate(HttpContext context); +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Resources.resx b/src/Http/Http.Abstractions/src/Resources.resx new file mode 100644 index 0000000000000000000000000000000000000000..dfdfeaf7d11e961ff8133bf2275743fe1f02b236 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Resources.resx @@ -0,0 +1,159 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="Exception_UseMiddlewareIServiceProviderNotAvailable" xml:space="preserve"> + <value>'{0}' is not available.</value> + </data> + <data name="Exception_UseMiddlewareNoInvokeMethod" xml:space="preserve"> + <value>No public '{0}' or '{1}' method found for middleware of type '{2}'.</value> + </data> + <data name="Exception_UseMiddlewareNonTaskReturnType" xml:space="preserve"> + <value>'{0}' or '{1}' does not return an object of type '{2}'.</value> + </data> + <data name="Exception_UseMiddlewareNoParameters" xml:space="preserve"> + <value>The '{0}' or '{1}' method's first argument must be of type '{2}'.</value> + </data> + <data name="Exception_UseMiddleMutlipleInvokes" xml:space="preserve"> + <value>Multiple public '{0}' or '{1}' methods are available.</value> + </data> + <data name="Exception_PathMustStartWithSlash" xml:space="preserve"> + <value>The path in '{0}' must start with '/'.</value> + </data> + <data name="Exception_InvokeMiddlewareNoService" xml:space="preserve"> + <value>Unable to resolve service for type '{0}' while attempting to Invoke middleware '{1}'.</value> + </data> + <data name="Exception_InvokeDoesNotSupportRefOrOutParams" xml:space="preserve"> + <value>The '{0}' method must not have ref or out parameters.</value> + </data> + <data name="Exception_PortMustBeGreaterThanZero" xml:space="preserve"> + <value>The value must be greater than zero.</value> + </data> + <data name="Exception_UseMiddlewareNoMiddlewareFactory" xml:space="preserve"> + <value>No service for type '{0}' has been registered.</value> + </data> + <data name="Exception_UseMiddlewareUnableToCreateMiddleware" xml:space="preserve"> + <value>'{0}' failed to create middleware of type '{1}'.</value> + </data> + <data name="Exception_UseMiddlewareExplicitArgumentsNotSupported" xml:space="preserve"> + <value>Types that implement '{0}' do not support explicit arguments.</value> + </data> + <data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve"> + <value>Argument cannot be null or empty.</value> + </data> +</root> \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/StatusCodes.cs b/src/Http/Http.Abstractions/src/StatusCodes.cs new file mode 100644 index 0000000000000000000000000000000000000000..3261bce2f2995d37c9c12d3b91c5774a2dbeb9f8 --- /dev/null +++ b/src/Http/Http.Abstractions/src/StatusCodes.cs @@ -0,0 +1,79 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http +{ + // Status Codes listed at http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + public static class StatusCodes + { + public const int Status100Continue = 100; + public const int Status101SwitchingProtocols = 101; + public const int Status102Processing = 102; + + public const int Status200OK = 200; + public const int Status201Created = 201; + public const int Status202Accepted = 202; + public const int Status203NonAuthoritative = 203; + public const int Status204NoContent = 204; + public const int Status205ResetContent = 205; + public const int Status206PartialContent = 206; + public const int Status207MultiStatus = 207; + public const int Status208AlreadyReported = 208; + public const int Status226IMUsed = 226; + + public const int Status300MultipleChoices = 300; + public const int Status301MovedPermanently = 301; + public const int Status302Found = 302; + public const int Status303SeeOther = 303; + public const int Status304NotModified = 304; + public const int Status305UseProxy = 305; + public const int Status306SwitchProxy = 306; // RFC 2616, removed + public const int Status307TemporaryRedirect = 307; + public const int Status308PermanentRedirect = 308; + + public const int Status400BadRequest = 400; + public const int Status401Unauthorized = 401; + public const int Status402PaymentRequired = 402; + public const int Status403Forbidden = 403; + public const int Status404NotFound = 404; + public const int Status405MethodNotAllowed = 405; + public const int Status406NotAcceptable = 406; + public const int Status407ProxyAuthenticationRequired = 407; + public const int Status408RequestTimeout = 408; + public const int Status409Conflict = 409; + public const int Status410Gone = 410; + public const int Status411LengthRequired = 411; + public const int Status412PreconditionFailed = 412; + public const int Status413RequestEntityTooLarge = 413; // RFC 2616, renamed + public const int Status413PayloadTooLarge = 413; // RFC 7231 + public const int Status414RequestUriTooLong = 414; // RFC 2616, renamed + public const int Status414UriTooLong = 414; // RFC 7231 + public const int Status415UnsupportedMediaType = 415; + public const int Status416RequestedRangeNotSatisfiable = 416; // RFC 2616, renamed + public const int Status416RangeNotSatisfiable = 416; // RFC 7233 + public const int Status417ExpectationFailed = 417; + public const int Status418ImATeapot = 418; + public const int Status419AuthenticationTimeout = 419; // Not defined in any RFC + public const int Status421MisdirectedRequest = 421; + public const int Status422UnprocessableEntity = 422; + public const int Status423Locked = 423; + public const int Status424FailedDependency = 424; + public const int Status426UpgradeRequired = 426; + public const int Status428PreconditionRequired = 428; + public const int Status429TooManyRequests = 429; + public const int Status431RequestHeaderFieldsTooLarge = 431; + public const int Status451UnavailableForLegalReasons = 451; + + public const int Status500InternalServerError = 500; + public const int Status501NotImplemented = 501; + public const int Status502BadGateway = 502; + public const int Status503ServiceUnavailable = 503; + public const int Status504GatewayTimeout = 504; + public const int Status505HttpVersionNotsupported = 505; + public const int Status506VariantAlsoNegotiates = 506; + public const int Status507InsufficientStorage = 507; + public const int Status508LoopDetected = 508; + public const int Status510NotExtended = 510; + public const int Status511NetworkAuthenticationRequired = 511; + } +} diff --git a/src/Http/Http.Abstractions/src/WebSocketManager.cs b/src/Http/Http.Abstractions/src/WebSocketManager.cs new file mode 100644 index 0000000000000000000000000000000000000000..79afefa5c0d4c18ab2faab05fd7fa1fc4743660e --- /dev/null +++ b/src/Http/Http.Abstractions/src/WebSocketManager.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Manages the establishment of WebSocket connections for a specific HTTP request. + /// </summary> + public abstract class WebSocketManager + { + /// <summary> + /// Gets a value indicating whether the request is a WebSocket establishment request. + /// </summary> + public abstract bool IsWebSocketRequest { get; } + + /// <summary> + /// Gets the list of requested WebSocket sub-protocols. + /// </summary> + public abstract IList<string> WebSocketRequestedProtocols { get; } + + /// <summary> + /// Transitions the request to a WebSocket connection. + /// </summary> + /// <returns>A task representing the completion of the transition.</returns> + public virtual Task<WebSocket> AcceptWebSocketAsync() + { + return AcceptWebSocketAsync(subProtocol: null); + } + + /// <summary> + /// Transitions the request to a WebSocket connection using the specified sub-protocol. + /// </summary> + /// <param name="subProtocol">The sub-protocol to use.</param> + /// <returns>A task representing the completion of the transition.</returns> + public abstract Task<WebSocket> AcceptWebSocketAsync(string subProtocol); + } +} diff --git a/src/Http/Http.Abstractions/src/baseline.netcore.json b/src/Http/Http.Abstractions/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..f407fb08e6338f31c1afd1a2bb5a1d38fab93ef4 --- /dev/null +++ b/src/Http/Http.Abstractions/src/baseline.netcore.json @@ -0,0 +1,5020 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Http.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Builder.MapExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Map", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "pathMatch", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "configuration", + "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.MapWhenExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "MapWhen", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "predicate", + "Type": "System.Func<Microsoft.AspNetCore.Http.HttpContext, System.Boolean>" + }, + { + "Name": "configuration", + "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.RunExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "handler", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.UseExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Use", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "middleware", + "Type": "System.Func<Microsoft.AspNetCore.Http.HttpContext, System.Func<System.Threading.Tasks.Task>, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.UseMiddlewareExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseMiddleware<T0>", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "args", + "Type": "System.Object[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TMiddleware", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "UseMiddleware", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "middleware", + "Type": "System.Type" + }, + { + "Name": "args", + "Type": "System.Object[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.UsePathBaseExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UsePathBase", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "pathBase", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.UseWhenExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseWhen", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "predicate", + "Type": "System.Func<Microsoft.AspNetCore.Http.HttpContext, System.Boolean>" + }, + { + "Name": "configuration", + "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ApplicationServices", + "Parameters": [], + "ReturnType": "System.IServiceProvider", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ApplicationServices", + "Parameters": [ + { + "Name": "value", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ServerFeatures", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, System.Object>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Use", + "Parameters": [ + { + "Name": "middleware", + "Type": "System.Func<Microsoft.AspNetCore.Http.RequestDelegate, Microsoft.AspNetCore.Http.RequestDelegate>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "New", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.RequestDelegate", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.Extensions.MapMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Builder.Extensions.MapOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.Extensions.MapOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_PathMatch", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PathMatch", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Branch", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.RequestDelegate", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Branch", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.Extensions.MapWhenMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Builder.Extensions.MapWhenOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.Extensions.MapWhenOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Predicate", + "Parameters": [], + "ReturnType": "System.Func<Microsoft.AspNetCore.Http.HttpContext, System.Boolean>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Predicate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func<Microsoft.AspNetCore.Http.HttpContext, System.Boolean>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Branch", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.RequestDelegate", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Branch", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.Extensions.UsePathBaseMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "pathBase", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.ConnectionInfo", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Id", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Id", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemoteIpAddress", + "Parameters": [], + "ReturnType": "System.Net.IPAddress", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemoteIpAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.IPAddress" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemotePort", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemotePort", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LocalIpAddress", + "Parameters": [], + "ReturnType": "System.Net.IPAddress", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LocalIpAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.IPAddress" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LocalPort", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LocalPort", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClientCertificate", + "Parameters": [], + "ReturnType": "System.Security.Cryptography.X509Certificates.X509Certificate2", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClientCertificate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Cryptography.X509Certificates.X509Certificate2" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetClientCertificateAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Security.Cryptography.X509Certificates.X509Certificate2>", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.CookieBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Name", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Path", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Domain", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Domain", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HttpOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HttpOnly", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SameSite", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.SameSiteMode", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SameSite", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.SameSiteMode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SecurePolicy", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieSecurePolicy", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SecurePolicy", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieSecurePolicy" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Expiration", + "Parameters": [], + "ReturnType": "System.Nullable<System.TimeSpan>", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Expiration", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.TimeSpan>" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxAge", + "Parameters": [], + "ReturnType": "System.Nullable<System.TimeSpan>", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.TimeSpan>" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsEssential", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsEssential", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "expiresFrom", + "Type": "System.DateTimeOffset" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.CookieSecurePolicy", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "SameAsRequest", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "Always", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + }, + { + "Kind": "Field", + "Name": "None", + "Parameters": [], + "GenericParameter": [], + "Literal": "2" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HeaderDictionaryExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Append", + "Parameters": [ + { + "Name": "headers", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendCommaSeparatedValues", + "Parameters": [ + { + "Name": "headers", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetCommaSeparatedValues", + "Parameters": [ + { + "Name": "headers", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.String[]", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetCommaSeparatedValues", + "Parameters": [ + { + "Name": "headers", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpResponseWritingExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "WriteAsync", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + }, + { + "Name": "text", + "Type": "System.String" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteAsync", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + }, + { + "Name": "text", + "Type": "System.String" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.FragmentString", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.IEquatable<Microsoft.AspNetCore.Http.FragmentString>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasValue", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToUriComponent", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uriComponent", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.FragmentString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uri", + "Type": "System.Uri" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.FragmentString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.FragmentString" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IEquatable<Microsoft.AspNetCore.Http.FragmentString>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Equality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.FragmentString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.FragmentString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Inequality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.FragmentString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.FragmentString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Empty", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.FragmentString", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HostString", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.IEquatable<Microsoft.AspNetCore.Http.HostString>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasValue", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Host", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Port", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int32>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToUriComponent", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uriComponent", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.HostString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uri", + "Type": "System.Uri" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.HostString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MatchesAny", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "patterns", + "Type": "System.Collections.Generic.IList<Microsoft.Extensions.Primitives.StringSegment>" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.HostString" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IEquatable<Microsoft.AspNetCore.Http.HostString>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Equality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.HostString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.HostString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Inequality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.HostString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.HostString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "host", + "Type": "System.String" + }, + { + "Name": "port", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpContext", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Features", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Request", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpRequest", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Response", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpResponse", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Connection", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.ConnectionInfo", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_WebSockets", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.WebSocketManager", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Authentication", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Authentication.AuthenticationManager", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_User", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Items", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.Object, System.Object>", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Items", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary<System.Object, System.Object>" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequestServices", + "Parameters": [], + "ReturnType": "System.IServiceProvider", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestServices", + "Parameters": [ + { + "Name": "value", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequestAborted", + "Parameters": [], + "ReturnType": "System.Threading.CancellationToken", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestAborted", + "Parameters": [ + { + "Name": "value", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TraceIdentifier", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TraceIdentifier", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Session", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.ISession", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Session", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.ISession" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Abort", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpMethods", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "IsConnect", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsDelete", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsGet", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsHead", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsOptions", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsPatch", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsPost", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsPut", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsTrace", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Connect", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Delete", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Get", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Head", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Options", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Patch", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Post", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Put", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Trace", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpRequest", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Method", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Method", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Scheme", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Scheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsHttps", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsHttps", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Host", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HostString", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Host", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.HostString" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PathBase", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PathBase", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Path", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_QueryString", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_QueryString", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Query", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IQueryCollection", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Query", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IQueryCollection" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Protocol", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Protocol", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Cookies", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IRequestCookieCollection", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Cookies", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IRequestCookieCollection" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentLength", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.Int64>" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentType", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentType", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Body", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Body", + "Parameters": [ + { + "Name": "value", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasFormContentType", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Form", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IFormCollection", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Form", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IFormCollection" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadFormAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Http.IFormCollection>", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpResponse", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StatusCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StatusCode", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Body", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Body", + "Parameters": [ + { + "Name": "value", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentLength", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.Int64>" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentType", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentType", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Cookies", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IResponseCookies", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasStarted", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStarting", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func<System.Object, System.Threading.Tasks.Task>" + }, + { + "Name": "state", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStarting", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func<System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnCompleted", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func<System.Object, System.Threading.Tasks.Task>" + }, + { + "Name": "state", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RegisterForDispose", + "Parameters": [ + { + "Name": "disposable", + "Type": "System.IDisposable" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnCompleted", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func<System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Redirect", + "Parameters": [ + { + "Name": "location", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Redirect", + "Parameters": [ + { + "Name": "location", + "Type": "System.String" + }, + { + "Name": "permanent", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IHttpContextAccessor", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HttpContext", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IHttpContextFactory", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Create", + "Parameters": [ + { + "Name": "featureCollection", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IMiddleware", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "InvokeAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IMiddlewareFactory", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Create", + "Parameters": [ + { + "Name": "middlewareType", + "Type": "System.Type" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.IMiddleware", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Release", + "Parameters": [ + { + "Name": "middleware", + "Type": "Microsoft.AspNetCore.Http.IMiddleware" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.IEquatable<Microsoft.AspNetCore.Http.PathString>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasValue", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToUriComponent", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uriComponent", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uri", + "Type": "System.Uri" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StartsWithSegments", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StartsWithSegments", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "comparisonType", + "Type": "System.StringComparison" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StartsWithSegments", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "remaining", + "Type": "Microsoft.AspNetCore.Http.PathString", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StartsWithSegments", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "comparisonType", + "Type": "System.StringComparison" + }, + { + "Name": "remaining", + "Type": "Microsoft.AspNetCore.Http.PathString", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StartsWithSegments", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "matched", + "Type": "Microsoft.AspNetCore.Http.PathString", + "Direction": "Out" + }, + { + "Name": "remaining", + "Type": "Microsoft.AspNetCore.Http.PathString", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StartsWithSegments", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "comparisonType", + "Type": "System.StringComparison" + }, + { + "Name": "matched", + "Type": "Microsoft.AspNetCore.Http.PathString", + "Direction": "Out" + }, + { + "Name": "remaining", + "Type": "Microsoft.AspNetCore.Http.PathString", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IEquatable<Microsoft.AspNetCore.Http.PathString>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "comparisonType", + "Type": "System.StringComparison" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Equality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Inequality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Addition", + "Parameters": [ + { + "Name": "left", + "Type": "System.String" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Addition", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "right", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Addition", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Addition", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Implicit", + "Parameters": [ + { + "Name": "s", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Implicit", + "Parameters": [ + { + "Name": "path", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Empty", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.QueryString", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.IEquatable<Microsoft.AspNetCore.Http.QueryString>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasValue", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToUriComponent", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uriComponent", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uri", + "Type": "System.Uri" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Create", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Create", + "Parameters": [ + { + "Name": "parameters", + "Type": "System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.String, System.String>>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Create", + "Parameters": [ + { + "Name": "parameters", + "Type": "System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IEquatable<Microsoft.AspNetCore.Http.QueryString>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Equality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.QueryString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Inequality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.QueryString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Addition", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.QueryString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Empty", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.RequestDelegate", + "Visibility": "Public", + "Kind": "Class", + "Sealed": true, + "BaseType": "System.MulticastDelegate", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "BeginInvoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "callback", + "Type": "System.AsyncCallback" + }, + { + "Name": "object", + "Type": "System.Object" + } + ], + "ReturnType": "System.IAsyncResult", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EndInvoke", + "Parameters": [ + { + "Name": "result", + "Type": "System.IAsyncResult" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "object", + "Type": "System.Object" + }, + { + "Name": "method", + "Type": "System.IntPtr" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.StatusCodes", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Status100Continue", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "100" + }, + { + "Kind": "Field", + "Name": "Status101SwitchingProtocols", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "101" + }, + { + "Kind": "Field", + "Name": "Status102Processing", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "102" + }, + { + "Kind": "Field", + "Name": "Status200OK", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "200" + }, + { + "Kind": "Field", + "Name": "Status201Created", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "201" + }, + { + "Kind": "Field", + "Name": "Status202Accepted", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "202" + }, + { + "Kind": "Field", + "Name": "Status203NonAuthoritative", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "203" + }, + { + "Kind": "Field", + "Name": "Status204NoContent", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "204" + }, + { + "Kind": "Field", + "Name": "Status205ResetContent", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "205" + }, + { + "Kind": "Field", + "Name": "Status206PartialContent", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "206" + }, + { + "Kind": "Field", + "Name": "Status207MultiStatus", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "207" + }, + { + "Kind": "Field", + "Name": "Status208AlreadyReported", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "208" + }, + { + "Kind": "Field", + "Name": "Status226IMUsed", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "226" + }, + { + "Kind": "Field", + "Name": "Status300MultipleChoices", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "300" + }, + { + "Kind": "Field", + "Name": "Status301MovedPermanently", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "301" + }, + { + "Kind": "Field", + "Name": "Status302Found", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "302" + }, + { + "Kind": "Field", + "Name": "Status303SeeOther", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "303" + }, + { + "Kind": "Field", + "Name": "Status304NotModified", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "304" + }, + { + "Kind": "Field", + "Name": "Status305UseProxy", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "305" + }, + { + "Kind": "Field", + "Name": "Status306SwitchProxy", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "306" + }, + { + "Kind": "Field", + "Name": "Status307TemporaryRedirect", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "307" + }, + { + "Kind": "Field", + "Name": "Status308PermanentRedirect", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "308" + }, + { + "Kind": "Field", + "Name": "Status400BadRequest", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "400" + }, + { + "Kind": "Field", + "Name": "Status401Unauthorized", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "401" + }, + { + "Kind": "Field", + "Name": "Status402PaymentRequired", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "402" + }, + { + "Kind": "Field", + "Name": "Status403Forbidden", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "403" + }, + { + "Kind": "Field", + "Name": "Status404NotFound", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "404" + }, + { + "Kind": "Field", + "Name": "Status405MethodNotAllowed", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "405" + }, + { + "Kind": "Field", + "Name": "Status406NotAcceptable", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "406" + }, + { + "Kind": "Field", + "Name": "Status407ProxyAuthenticationRequired", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "407" + }, + { + "Kind": "Field", + "Name": "Status408RequestTimeout", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "408" + }, + { + "Kind": "Field", + "Name": "Status409Conflict", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "409" + }, + { + "Kind": "Field", + "Name": "Status410Gone", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "410" + }, + { + "Kind": "Field", + "Name": "Status411LengthRequired", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "411" + }, + { + "Kind": "Field", + "Name": "Status412PreconditionFailed", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "412" + }, + { + "Kind": "Field", + "Name": "Status413RequestEntityTooLarge", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "413" + }, + { + "Kind": "Field", + "Name": "Status413PayloadTooLarge", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "413" + }, + { + "Kind": "Field", + "Name": "Status414RequestUriTooLong", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "414" + }, + { + "Kind": "Field", + "Name": "Status414UriTooLong", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "414" + }, + { + "Kind": "Field", + "Name": "Status415UnsupportedMediaType", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "415" + }, + { + "Kind": "Field", + "Name": "Status416RequestedRangeNotSatisfiable", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "416" + }, + { + "Kind": "Field", + "Name": "Status416RangeNotSatisfiable", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "416" + }, + { + "Kind": "Field", + "Name": "Status417ExpectationFailed", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "417" + }, + { + "Kind": "Field", + "Name": "Status418ImATeapot", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "418" + }, + { + "Kind": "Field", + "Name": "Status419AuthenticationTimeout", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "419" + }, + { + "Kind": "Field", + "Name": "Status421MisdirectedRequest", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "421" + }, + { + "Kind": "Field", + "Name": "Status422UnprocessableEntity", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "422" + }, + { + "Kind": "Field", + "Name": "Status423Locked", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "423" + }, + { + "Kind": "Field", + "Name": "Status424FailedDependency", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "424" + }, + { + "Kind": "Field", + "Name": "Status426UpgradeRequired", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "426" + }, + { + "Kind": "Field", + "Name": "Status428PreconditionRequired", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "428" + }, + { + "Kind": "Field", + "Name": "Status429TooManyRequests", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "429" + }, + { + "Kind": "Field", + "Name": "Status431RequestHeaderFieldsTooLarge", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "431" + }, + { + "Kind": "Field", + "Name": "Status451UnavailableForLegalReasons", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "451" + }, + { + "Kind": "Field", + "Name": "Status500InternalServerError", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "500" + }, + { + "Kind": "Field", + "Name": "Status501NotImplemented", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "501" + }, + { + "Kind": "Field", + "Name": "Status502BadGateway", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "502" + }, + { + "Kind": "Field", + "Name": "Status503ServiceUnavailable", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "503" + }, + { + "Kind": "Field", + "Name": "Status504GatewayTimeout", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "504" + }, + { + "Kind": "Field", + "Name": "Status505HttpVersionNotsupported", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "505" + }, + { + "Kind": "Field", + "Name": "Status506VariantAlsoNegotiates", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "506" + }, + { + "Kind": "Field", + "Name": "Status507InsufficientStorage", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "507" + }, + { + "Kind": "Field", + "Name": "Status508LoopDetected", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "508" + }, + { + "Kind": "Field", + "Name": "Status510NotExtended", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "510" + }, + { + "Kind": "Field", + "Name": "Status511NetworkAuthenticationRequired", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "511" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.WebSocketManager", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsWebSocketRequest", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_WebSocketRequestedProtocols", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList<System.String>", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AcceptWebSocketAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<System.Net.WebSockets.WebSocket>", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AcceptWebSocketAsync", + "Parameters": [ + { + "Name": "subProtocol", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Net.WebSockets.WebSocket>", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Authentication.AuthenticateInfo", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Principal", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Properties", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Description", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Authentication.AuthenticationDescription", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Description", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationDescription" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Authentication.AuthenticationDescription", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Items", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, System.Object>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthenticationScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DisplayName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Authentication.AuthenticationManager", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetAuthenticationSchemes", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Http.Authentication.AuthenticationDescription>", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetAuthenticateInfoAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Http.Authentication.AuthenticateInfo>", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.AuthenticateContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Security.Claims.ClaimsPrincipal>", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + }, + { + "Name": "behavior", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.ChallengeBehavior" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AutomaticScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Automatic\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Items", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, System.String>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsPersistent", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsPersistent", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RedirectUri", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RedirectUri", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IssuedUtc", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IssuedUtc", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ExpiresUtc", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ExpiresUtc", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AllowRefresh", + "Parameters": [], + "ReturnType": "System.Nullable<System.Boolean>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AllowRefresh", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.Boolean>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary<System.String, System.String>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/test/CookieBuilderTests.cs b/src/Http/Http.Abstractions/test/CookieBuilderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..dd540ccc1ba012ac2d8d7083e0f9e66267644da7 --- /dev/null +++ b/src/Http/Http.Abstractions/test/CookieBuilderTests.cs @@ -0,0 +1,57 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Http.Abstractions.Tests +{ + public class CookieBuilderTests + { + [Theory] + [InlineData(CookieSecurePolicy.Always, false, true)] + [InlineData(CookieSecurePolicy.Always, true, true)] + [InlineData(CookieSecurePolicy.SameAsRequest, true, true)] + [InlineData(CookieSecurePolicy.SameAsRequest, false, false)] + [InlineData(CookieSecurePolicy.None, true, false)] + [InlineData(CookieSecurePolicy.None, false, false)] + public void ConfiguresSecurePolicy(CookieSecurePolicy policy, bool requestIsHttps, bool secure) + { + var builder = new CookieBuilder + { + SecurePolicy = policy + }; + var context = new DefaultHttpContext(); + context.Request.IsHttps = requestIsHttps; + var options = builder.Build(context); + + Assert.Equal(secure, options.Secure); + } + + [Fact] + public void ComputesExpiration() + { + Assert.Null(new CookieBuilder().Build(new DefaultHttpContext()).Expires); + + var now = DateTimeOffset.Now; + var options = new CookieBuilder { Expiration = TimeSpan.FromHours(1) }.Build(new DefaultHttpContext(), now); + Assert.Equal(now.AddHours(1), options.Expires); + } + + [Fact] + public void ComputesMaxAge() + { + Assert.Null(new CookieBuilder().Build(new DefaultHttpContext()).MaxAge); + + var now = TimeSpan.FromHours(1); + var options = new CookieBuilder { MaxAge = now }.Build(new DefaultHttpContext()); + Assert.Equal(now, options.MaxAge); + } + + [Fact] + public void CookieBuilderPreservesDefaultPath() + { + Assert.Equal(new CookieOptions().Path, new CookieBuilder().Build(new DefaultHttpContext()).Path); + } + } +} diff --git a/src/Http/Http.Abstractions/test/FragmentStringTests.cs b/src/Http/Http.Abstractions/test/FragmentStringTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..4f5fe20916ca5370d1914b2f94ed48c350dfcb0d --- /dev/null +++ b/src/Http/Http.Abstractions/test/FragmentStringTests.cs @@ -0,0 +1,41 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Http.Abstractions.Tests +{ + public class FragmentStringTests + { + [Fact] + public void Equals_EmptyFragmentStringAndDefaultFragmentString() + { + // Act and Assert + Assert.Equal(default(FragmentString), FragmentString.Empty); + Assert.Equal(default(FragmentString), FragmentString.Empty); + // explicitly checking == operator + Assert.True(FragmentString.Empty == default(FragmentString)); + Assert.True(default(FragmentString) == FragmentString.Empty); + } + + [Fact] + public void NotEquals_DefaultFragmentStringAndNonNullFragmentString() + { + // Arrange + var fragmentString = new FragmentString("#col=1"); + + // Act and Assert + Assert.NotEqual(default(FragmentString), fragmentString); + } + + [Fact] + public void NotEquals_EmptyFragmentStringAndNonNullFragmentString() + { + // Arrange + var fragmentString = new FragmentString("#col=1"); + + // Act and Assert + Assert.NotEqual(fragmentString, FragmentString.Empty); + } + } +} diff --git a/src/Http/Http.Abstractions/test/HostStringTest.cs b/src/Http/Http.Abstractions/test/HostStringTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..85820f8ffcae1d76e55116adf964dcdc8abeb1c7 --- /dev/null +++ b/src/Http/Http.Abstractions/test/HostStringTest.cs @@ -0,0 +1,175 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class HostStringTests + { + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void CtorThrows_IfPortIsNotGreaterThanZero(int port) + { + // Act and Assert + ExceptionAssert.ThrowsArgumentOutOfRange(() => new HostString("localhost", port), "port", "The value must be greater than zero."); + } + + [Theory] + [InlineData("localhost", "localhost")] + [InlineData("1.2.3.4", "1.2.3.4")] + [InlineData("[2001:db8:a0b:12f0::1]", "[2001:db8:a0b:12f0::1]")] + [InlineData("本地主機", "本地主機")] + [InlineData("localhost:5000", "localhost")] + [InlineData("1.2.3.4:5000", "1.2.3.4")] + [InlineData("[2001:db8:a0b:12f0::1]:5000", "[2001:db8:a0b:12f0::1]")] + [InlineData("本地主機:5000", "本地主機")] + public void Domain_ExtractsHostFromValue(string sourceValue, string expectedDomain) + { + // Arrange + var hostString = new HostString(sourceValue); + + // Act + var result = hostString.Host; + + // Assert + Assert.Equal(expectedDomain, result); + } + + [Theory] + [InlineData("localhost", null)] + [InlineData("1.2.3.4", null)] + [InlineData("[2001:db8:a0b:12f0::1]", null)] + [InlineData("本地主機", null)] + [InlineData("localhost:5000", 5000)] + [InlineData("1.2.3.4:5000", 5000)] + [InlineData("[2001:db8:a0b:12f0::1]:5000", 5000)] + [InlineData("本地主機:5000", 5000)] + public void Port_ExtractsPortFromValue(string sourceValue, int? expectedPort) + { + // Arrange + var hostString = new HostString(sourceValue); + + // Act + var result = hostString.Port; + + // Assert + Assert.Equal(expectedPort, result); + } + + [Theory] + [InlineData("localhost:BLAH")] + public void Port_ExtractsInvalidPortFromValue(string sourceValue) + { + // Arrange + var hostString = new HostString(sourceValue); + + // Act + var result = hostString.Port; + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("localhost", 5000, "localhost", 5000)] + [InlineData("1.2.3.4", 5000, "1.2.3.4", 5000)] + [InlineData("[2001:db8:a0b:12f0::1]", 5000, "[2001:db8:a0b:12f0::1]", 5000)] + [InlineData("2001:db8:a0b:12f0::1", 5000, "[2001:db8:a0b:12f0::1]", 5000)] + [InlineData("本地主機", 5000, "本地主機", 5000)] + public void Ctor_CreatesFromHostAndPort(string sourceHost, int sourcePort, string expectedHost, int expectedPort) + { + // Arrange + var hostString = new HostString(sourceHost, sourcePort); + + // Act + var host = hostString.Host; + var port = hostString.Port; + + // Assert + Assert.Equal(expectedHost, host); + Assert.Equal(expectedPort, port); + } + + [Fact] + public void Equals_EmptyHostStringAndDefaultHostString() + { + // Act and Assert + Assert.Equal(default(HostString), new HostString(string.Empty)); + Assert.Equal(default(HostString), new HostString(string.Empty)); + // explicitly checking == operator + Assert.True(new HostString(string.Empty) == default(HostString)); + Assert.True(default(HostString) == new HostString(string.Empty)); + } + + [Fact] + public void NotEquals_DefaultHostStringAndNonNullHostString() + { + // Arrange + var hostString = new HostString("example.com"); + + // Act and Assert + Assert.NotEqual(default(HostString), hostString); + } + + [Fact] + public void NotEquals_EmptyHostStringAndNonNullHostString() + { + // Arrange + var hostString = new HostString("example.com"); + + // Act and Assert + Assert.NotEqual(hostString, new HostString(string.Empty)); + } + + [Theory] + [InlineData("localHost", "localhost")] + [InlineData("localHost", "*")] // Any - Used by HttpSys + [InlineData("localhost:9090", "localHost")] + [InlineData("example.com:443", "example.com")] + [InlineData("foo.eXample.com:443", "*.exampLe.com")] + [InlineData("f.eXample.com:443", "*.exampLe.com")] + [InlineData("a.b.c.eXample.com:443", "*.exampLe.com")] + [InlineData("127.0.0.1", "127.0.0.1")] + [InlineData("127.0.0.1:443", "127.0.0.1")] + [InlineData("xn--c1yn36f:443", "xn--c1yn36f")] + [InlineData("點看", "點看")] + [InlineData("[::ABC]", "[::aBc]")] + [InlineData("[::1]:80", "[::1]")] + [InlineData("[::1]:", "[::1]")] + [InlineData("::1", "[::1]")] + public void HostMatches(string host, string pattern) + { + Assert.True(HostString.MatchesAny(host, new StringSegment[] { pattern })); + } + + [Theory] + [InlineData("example.com", "localhost")] + [InlineData("localhost:9090", "example.com")] + [InlineData(":80", "localhost")] + [InlineData(":", "localhost")] + [InlineData("example.com:443", "*.example.com")] + [InlineData(".example.com:443", "*.example.com")] + [InlineData("foo.com:443", "*.example.com")] + [InlineData("foo.example.com.bar:443", "*.example.com")] + [InlineData(".com:443", "*.com")] + [InlineData("xn--c1yn36f:443", "點看")] + [InlineData("[::1", "[::1]")] + [InlineData("[::1:80", "[::1]")] + [InlineData("::1", "::1")] // Brackets are added to the host before the comparison + public void HostDoesntMatch(string host, string pattern) + { + Assert.False(HostString.MatchesAny(host, new StringSegment[] { pattern })); + } + + [Fact] + public void HostMatchThrowsForBadPort() + { + Assert.Throws<FormatException>(() => HostString.MatchesAny("example.com:1abc", new StringSegment[] { "example.com" })); + } + } +} diff --git a/src/Http/Http.Abstractions/test/HttpResponseWritingExtensionsTests.cs b/src/Http/Http.Abstractions/test/HttpResponseWritingExtensionsTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..f8e9e27d1ce9a0ba14771de3505268e58303f9f8 --- /dev/null +++ b/src/Http/Http.Abstractions/test/HttpResponseWritingExtensionsTests.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class HttpResponseWritingExtensionsTests + { + [Fact] + public async Task WritingText_WriteText() + { + HttpContext context = CreateRequest(); + await context.Response.WriteAsync("Hello World"); + + Assert.Equal(11, context.Response.Body.Length); + } + + [Fact] + public async Task WritingText_MultipleWrites() + { + HttpContext context = CreateRequest(); + await context.Response.WriteAsync("Hello World"); + await context.Response.WriteAsync("Hello World"); + + Assert.Equal(22, context.Response.Body.Length); + } + + private HttpContext CreateRequest() + { + HttpContext context = new DefaultHttpContext(); + context.Response.Body = new MemoryStream(); + return context; + } + } +} diff --git a/src/Http/Http.Abstractions/test/MapPathMiddlewareTests.cs b/src/Http/Http.Abstractions/test/MapPathMiddlewareTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..a30e99603ca1952668b0fc08de369e0d94afc8c1 --- /dev/null +++ b/src/Http/Http.Abstractions/test/MapPathMiddlewareTests.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; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Builder.Extensions +{ + public class MapPathMiddlewareTests + { + private static readonly Action<IApplicationBuilder> ActionNotImplemented = new Action<IApplicationBuilder>(_ => { throw new NotImplementedException(); }); + + private static Task Success(HttpContext context) + { + context.Response.StatusCode = 200; + context.Items["test.PathBase"] = context.Request.PathBase.Value; + context.Items["test.Path"] = context.Request.Path.Value; + return Task.FromResult<object>(null); + } + + private static void UseSuccess(IApplicationBuilder app) + { + app.Run(Success); + } + + private static Task NotImplemented(HttpContext context) + { + throw new NotImplementedException(); + } + + private static void UseNotImplemented(IApplicationBuilder app) + { + app.Run(NotImplemented); + } + + [Fact] + public void NullArguments_ArgumentNullException() + { + var builder = new ApplicationBuilder(serviceProvider: null); + var noMiddleware = new ApplicationBuilder(serviceProvider: null).Build(); + var noOptions = new MapOptions(); + Assert.Throws<ArgumentNullException>(() => builder.Map("/foo", configuration: null)); + Assert.Throws<ArgumentNullException>(() => new MapMiddleware(noMiddleware, null)); + } + + [Theory] + [InlineData("/foo", "", "/foo")] + [InlineData("/foo", "", "/foo/")] + [InlineData("/foo", "/Bar", "/foo")] + [InlineData("/foo", "/Bar", "/foo/cho")] + [InlineData("/foo", "/Bar", "/foo/cho/")] + [InlineData("/foo/cho", "/Bar", "/foo/cho")] + [InlineData("/foo/cho", "/Bar", "/foo/cho/do")] + public void PathMatchFunc_BranchTaken(string matchPath, string basePath, string requestPath) + { + HttpContext context = CreateRequest(basePath, requestPath); + var builder = new ApplicationBuilder(serviceProvider: null); + builder.Map(matchPath, UseSuccess); + var app = builder.Build(); + app.Invoke(context).Wait(); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(basePath, context.Request.PathBase.Value); + Assert.Equal(requestPath, context.Request.Path.Value); + } + + [Theory] + [InlineData("/foo", "", "/foo")] + [InlineData("/foo", "", "/foo/")] + [InlineData("/foo", "/Bar", "/foo")] + [InlineData("/foo", "/Bar", "/foo/cho")] + [InlineData("/foo", "/Bar", "/foo/cho/")] + [InlineData("/foo/cho", "/Bar", "/foo/cho")] + [InlineData("/foo/cho", "/Bar", "/foo/cho/do")] + [InlineData("/foo", "", "/Foo")] + [InlineData("/foo", "", "/Foo/")] + [InlineData("/foo", "/Bar", "/Foo")] + [InlineData("/foo", "/Bar", "/Foo/Cho")] + [InlineData("/foo", "/Bar", "/Foo/Cho/")] + [InlineData("/foo/cho", "/Bar", "/Foo/Cho")] + [InlineData("/foo/cho", "/Bar", "/Foo/Cho/do")] + public void PathMatchAction_BranchTaken(string matchPath, string basePath, string requestPath) + { + HttpContext context = CreateRequest(basePath, requestPath); + var builder = new ApplicationBuilder(serviceProvider: null); + builder.Map(matchPath, subBuilder => subBuilder.Run(Success)); + var app = builder.Build(); + app.Invoke(context).Wait(); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(basePath + requestPath.Substring(0, matchPath.Length), (string)context.Items["test.PathBase"]); + Assert.Equal(requestPath.Substring(matchPath.Length), context.Items["test.Path"]); + } + + [Theory] + [InlineData("/")] + [InlineData("/foo/")] + [InlineData("/foo/cho/")] + public void MatchPathWithTrailingSlashThrowsException(string matchPath) + { + Assert.Throws<ArgumentException>(() => new ApplicationBuilder(serviceProvider: null).Map(matchPath, map => { }).Build()); + } + + [Theory] + [InlineData("/foo", "", "")] + [InlineData("/foo", "/bar", "")] + [InlineData("/foo", "", "/bar")] + [InlineData("/foo", "/foo", "")] + [InlineData("/foo", "/foo", "/bar")] + [InlineData("/foo", "", "/bar/foo")] + [InlineData("/foo/bar", "/foo", "/bar")] + public void PathMismatchFunc_PassedThrough(string matchPath, string basePath, string requestPath) + { + HttpContext context = CreateRequest(basePath, requestPath); + var builder = new ApplicationBuilder(serviceProvider: null); + builder.Map(matchPath, UseNotImplemented); + builder.Run(Success); + var app = builder.Build(); + app.Invoke(context).Wait(); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(basePath, context.Request.PathBase.Value); + Assert.Equal(requestPath, context.Request.Path.Value); + } + + [Theory] + [InlineData("/foo", "", "")] + [InlineData("/foo", "/bar", "")] + [InlineData("/foo", "", "/bar")] + [InlineData("/foo", "/foo", "")] + [InlineData("/foo", "/foo", "/bar")] + [InlineData("/foo", "", "/bar/foo")] + [InlineData("/foo/bar", "/foo", "/bar")] + public void PathMismatchAction_PassedThrough(string matchPath, string basePath, string requestPath) + { + HttpContext context = CreateRequest(basePath, requestPath); + var builder = new ApplicationBuilder(serviceProvider: null); + builder.Map(matchPath, UseNotImplemented); + builder.Run(Success); + var app = builder.Build(); + app.Invoke(context).Wait(); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(basePath, context.Request.PathBase.Value); + Assert.Equal(requestPath, context.Request.Path.Value); + } + + [Fact] + public void ChainedRoutes_Success() + { + var builder = new ApplicationBuilder(serviceProvider: null); + builder.Map("/route1", map => + { + map.Map("/subroute1", UseSuccess); + map.Run(NotImplemented); + }); + builder.Map("/route2/subroute2", UseSuccess); + var app = builder.Build(); + + HttpContext context = CreateRequest(string.Empty, "/route1"); + Assert.Throws<AggregateException>(() => app.Invoke(context).Wait()); + + context = CreateRequest(string.Empty, "/route1/subroute1"); + app.Invoke(context).Wait(); + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Request.PathBase.Value); + Assert.Equal("/route1/subroute1", context.Request.Path.Value); + + context = CreateRequest(string.Empty, "/route2"); + app.Invoke(context).Wait(); + Assert.Equal(404, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Request.PathBase.Value); + Assert.Equal("/route2", context.Request.Path.Value); + + context = CreateRequest(string.Empty, "/route2/subroute2"); + app.Invoke(context).Wait(); + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Request.PathBase.Value); + Assert.Equal("/route2/subroute2", context.Request.Path.Value); + + context = CreateRequest(string.Empty, "/route2/subroute2/subsub2"); + app.Invoke(context).Wait(); + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Request.PathBase.Value); + Assert.Equal("/route2/subroute2/subsub2", context.Request.Path.Value); + } + + private HttpContext CreateRequest(string basePath, string requestPath) + { + HttpContext context = new DefaultHttpContext(); + context.Request.PathBase = new PathString(basePath); + context.Request.Path = new PathString(requestPath); + return context; + } + } +} diff --git a/src/Http/Http.Abstractions/test/MapPredicateMiddlewareTests.cs b/src/Http/Http.Abstractions/test/MapPredicateMiddlewareTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..0313a730d51c83a7d47ed23dc2bd91a6831f095f --- /dev/null +++ b/src/Http/Http.Abstractions/test/MapPredicateMiddlewareTests.cs @@ -0,0 +1,123 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Builder.Extensions +{ + using Predicate = Func<HttpContext, bool>; + + public class MapPredicateMiddlewareTests + { + private static readonly Predicate NotImplementedPredicate = new Predicate(envionment => { throw new NotImplementedException(); }); + + private static Task Success(HttpContext context) + { + context.Response.StatusCode = 200; + return Task.FromResult<object>(null); + } + + private static void UseSuccess(IApplicationBuilder app) + { + app.Run(Success); + } + + private static Task NotImplemented(HttpContext context) + { + throw new NotImplementedException(); + } + + private static void UseNotImplemented(IApplicationBuilder app) + { + app.Run(NotImplemented); + } + + private bool TruePredicate(HttpContext context) + { + return true; + } + + private bool FalsePredicate(HttpContext context) + { + return false; + } + + [Fact] + public void NullArguments_ArgumentNullException() + { + var builder = new ApplicationBuilder(serviceProvider: null); + var noMiddleware = new ApplicationBuilder(serviceProvider: null).Build(); + var noOptions = new MapWhenOptions(); + Assert.Throws<ArgumentNullException>(() => builder.MapWhen(null, UseNotImplemented)); + Assert.Throws<ArgumentNullException>(() => builder.MapWhen(NotImplementedPredicate, configuration: null)); + Assert.Throws<ArgumentNullException>(() => new MapWhenMiddleware(null, noOptions)); + Assert.Throws<ArgumentNullException>(() => new MapWhenMiddleware(noMiddleware, null)); + Assert.Throws<ArgumentNullException>(() => new MapWhenMiddleware(null, noOptions)); + Assert.Throws<ArgumentNullException>(() => new MapWhenMiddleware(noMiddleware, null)); + } + + [Fact] + public void PredicateTrue_BranchTaken() + { + HttpContext context = CreateRequest(); + var builder = new ApplicationBuilder(serviceProvider: null); + builder.MapWhen(TruePredicate, UseSuccess); + var app = builder.Build(); + app.Invoke(context).Wait(); + + Assert.Equal(200, context.Response.StatusCode); + } + + [Fact] + public void PredicateTrueAction_BranchTaken() + { + HttpContext context = CreateRequest(); + var builder = new ApplicationBuilder(serviceProvider: null); + builder.MapWhen(TruePredicate, UseSuccess); + var app = builder.Build(); + app.Invoke(context).Wait(); + + Assert.Equal(200, context.Response.StatusCode); + } + + [Fact] + public void PredicateFalseAction_PassThrough() + { + HttpContext context = CreateRequest(); + var builder = new ApplicationBuilder(serviceProvider: null); + builder.MapWhen(FalsePredicate, UseNotImplemented); + builder.Run(Success); + var app = builder.Build(); + app.Invoke(context).Wait(); + + Assert.Equal(200, context.Response.StatusCode); + } + + [Fact] + public void ChainedPredicates_Success() + { + var builder = new ApplicationBuilder(serviceProvider: null); + builder.MapWhen(TruePredicate, map1 => + { + map1.MapWhen((Predicate)FalsePredicate, UseNotImplemented); + map1.MapWhen((Predicate)TruePredicate, map2 => map2.MapWhen((Predicate)TruePredicate, UseSuccess)); + map1.Run(NotImplemented); + }); + var app = builder.Build(); + + HttpContext context = CreateRequest(); + app.Invoke(context).Wait(); + Assert.Equal(200, context.Response.StatusCode); + } + + private HttpContext CreateRequest() + { + HttpContext context = new DefaultHttpContext(); + return context; + } + } +} diff --git a/src/Http/Http.Abstractions/test/Microsoft.AspNetCore.Http.Abstractions.Tests.csproj b/src/Http/Http.Abstractions/test/Microsoft.AspNetCore.Http.Abstractions.Tests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..a97c164925e7595210b08dcd691551de55a5de34 --- /dev/null +++ b/src/Http/Http.Abstractions/test/Microsoft.AspNetCore.Http.Abstractions.Tests.csproj @@ -0,0 +1,11 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Http" /> + </ItemGroup> + +</Project> diff --git a/src/Http/Http.Abstractions/test/PathStringTests.cs b/src/Http/Http.Abstractions/test/PathStringTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..2d3c6e23f09088c837c5403fed35a6d452d402d4 --- /dev/null +++ b/src/Http/Http.Abstractions/test/PathStringTests.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.ComponentModel; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class PathStringTests + { + [Fact] + public void CtorThrows_IfPathDoesNotHaveLeadingSlash() + { + // Act and Assert + ExceptionAssert.ThrowsArgument(() => new PathString("hello"), "value", "The path in 'value' must start with '/'."); + } + + [Fact] + public void Equals_EmptyPathStringAndDefaultPathString() + { + // Act and Assert + Assert.Equal(default(PathString), PathString.Empty); + Assert.Equal(default(PathString), PathString.Empty); + Assert.True(PathString.Empty == default(PathString)); + Assert.True(default(PathString) == PathString.Empty); + Assert.True(PathString.Empty.Equals(default(PathString))); + Assert.True(default(PathString).Equals(PathString.Empty)); + } + + [Fact] + public void NotEquals_DefaultPathStringAndNonNullPathString() + { + // Arrange + var pathString = new PathString("/hello"); + + // Act and Assert + Assert.NotEqual(default(PathString), pathString); + } + + [Fact] + public void NotEquals_EmptyPathStringAndNonNullPathString() + { + // Arrange + var pathString = new PathString("/hello"); + + // Act and Assert + Assert.NotEqual(pathString, PathString.Empty); + } + + [Fact] + public void HashCode_CheckNullAndEmptyHaveSameHashcodes() + { + Assert.Equal(PathString.Empty.GetHashCode(), default(PathString).GetHashCode()); + } + + [Theory] + [InlineData(null, null)] + [InlineData("", null)] + public void AddPathString_HandlesNullAndEmptyStrings(string appString, string concatString) + { + // Arrange + var appPath = new PathString(appString); + var concatPath = new PathString(concatString); + + // Act + var result = appPath.Add(concatPath); + + // Assert + Assert.False(result.HasValue); + } + + [Theory] + [InlineData("", "/", "/")] + [InlineData("/", null, "/")] + [InlineData("/", "", "/")] + [InlineData("/", "/test", "/test")] + [InlineData("/myapp/", "/test/bar", "/myapp/test/bar")] + [InlineData("/myapp/", "/test/bar/", "/myapp/test/bar/")] + public void AddPathString_HandlesLeadingAndTrailingSlashes(string appString, string concatString, string expected) + { + // Arrange + var appPath = new PathString(appString); + var concatPath = new PathString(concatString); + + // Act + var result = appPath.Add(concatPath); + + // Assert + Assert.Equal(expected, result.Value); + } + + [Fact] + public void ImplicitStringConverters_WorksWithAdd() + { + var scheme = "http"; + var host = new HostString("localhost:80"); + var pathBase = new PathString("/base"); + var path = new PathString("/path"); + var query = new QueryString("?query"); + var fragment = new FragmentString("#frag"); + + var result = scheme + "://" + host + pathBase + path + query + fragment; + Assert.Equal("http://localhost:80/base/path?query#frag", result); + + result = pathBase + path + query + fragment; + Assert.Equal("/base/path?query#frag", result); + + result = path + "text"; + Assert.Equal("/pathtext", result); + } + + [Theory] + [InlineData("/test/path", "/TEST", true)] + [InlineData("/test/path", "/TEST/pa", false)] + [InlineData("/TEST/PATH", "/test", true)] + [InlineData("/TEST/path", "/test/pa", false)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", true)] + public void StartsWithSegments_DoesACaseInsensitiveMatch(string sourcePath, string testPath, bool expectedResult) + { + var source = new PathString(sourcePath); + var test = new PathString(testPath); + + var result = source.StartsWithSegments(test); + + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("/test/path", "/TEST", true)] + [InlineData("/test/path", "/TEST/pa", false)] + [InlineData("/TEST/PATH", "/test", true)] + [InlineData("/TEST/path", "/test/pa", false)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", true)] + public void StartsWithSegmentsWithRemainder_DoesACaseInsensitiveMatch(string sourcePath, string testPath, bool expectedResult) + { + var source = new PathString(sourcePath); + var test = new PathString(testPath); + + var result = source.StartsWithSegments(test, out var remaining); + + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("/test/path", "/TEST", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/test/path", "/TEST", StringComparison.Ordinal, false)] + [InlineData("/test/path", "/TEST/pa", StringComparison.OrdinalIgnoreCase, false)] + [InlineData("/test/path", "/TEST/pa", StringComparison.Ordinal, false)] + [InlineData("/TEST/PATH", "/test", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/TEST/PATH", "/test", StringComparison.Ordinal, false)] + [InlineData("/TEST/path", "/test/pa", StringComparison.OrdinalIgnoreCase, false)] + [InlineData("/TEST/path", "/test/pa", StringComparison.Ordinal, false)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.Ordinal, false)] + public void StartsWithSegments_DoesMatchUsingSpecifiedComparison(string sourcePath, string testPath, StringComparison comparison, bool expectedResult) + { + var source = new PathString(sourcePath); + var test = new PathString(testPath); + + var result = source.StartsWithSegments(test, comparison); + + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("/test/path", "/TEST", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/test/path", "/TEST", StringComparison.Ordinal, false)] + [InlineData("/test/path", "/TEST/pa", StringComparison.OrdinalIgnoreCase, false)] + [InlineData("/test/path", "/TEST/pa", StringComparison.Ordinal, false)] + [InlineData("/TEST/PATH", "/test", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/TEST/PATH", "/test", StringComparison.Ordinal, false)] + [InlineData("/TEST/path", "/test/pa", StringComparison.OrdinalIgnoreCase, false)] + [InlineData("/TEST/path", "/test/pa", StringComparison.Ordinal, false)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.Ordinal, false)] + public void StartsWithSegmentsWithRemainder_DoesMatchUsingSpecifiedComparison(string sourcePath, string testPath, StringComparison comparison, bool expectedResult) + { + var source = new PathString(sourcePath); + var test = new PathString(testPath); + + var result = source.StartsWithSegments(test, comparison, out var remaining); + + Assert.Equal(expectedResult, result); + } + + [Theory] + // unreserved + [InlineData("/abc123.-_~", "/abc123.-_~")] + // colon + [InlineData("/:", "/:")] + // at + [InlineData("/@", "/@")] + // sub-delims + [InlineData("/!$&'()*+,;=", "/!$&'()*+,;=")] + // reserved + [InlineData("/?#[]", "/%3F%23%5B%5D")] + // pct-encoding + [InlineData("/å•è¡Œé“", "/%E5%8D%95%E8%A1%8C%E9%81%93")] + // mixed + [InlineData("/index/å•è¡Œé“=(x*y)[abc]", "/index/%E5%8D%95%E8%A1%8C%E9%81%93=(x*y)%5Babc%5D")] + [InlineData("/index/å•è¡Œé“=(x*y)[abc]_", "/index/%E5%8D%95%E8%A1%8C%E9%81%93=(x*y)%5Babc%5D_")] + // encoded + [InlineData("/http%3a%2f%2f[foo]%3A5000/", "/http%3a%2f%2f%5Bfoo%5D%3A5000/")] + [InlineData("/http%3a%2f%2f[foo]%3A5000/%", "/http%3a%2f%2f%5Bfoo%5D%3A5000/%25")] + [InlineData("/http%3a%2f%2f[foo]%3A5000/%2", "/http%3a%2f%2f%5Bfoo%5D%3A5000/%252")] + [InlineData("/http%3a%2f%2f[foo]%3A5000/%2F", "/http%3a%2f%2f%5Bfoo%5D%3A5000/%2F")] + public void ToUriComponentEscapeCorrectly(string input, string expected) + { + var path = new PathString(input); + + Assert.Equal(expected, path.ToUriComponent()); + } + + [Fact] + public void PathStringConvertsOnlyToAndFromString() + { + var converter = TypeDescriptor.GetConverter(typeof(PathString)); + PathString result = (PathString)converter.ConvertFromInvariantString("/foo"); + Assert.Equal("/foo", result.ToString()); + Assert.Equal("/foo", converter.ConvertTo(result, typeof(string))); + Assert.True(converter.CanConvertFrom(typeof(string))); + Assert.False(converter.CanConvertFrom(typeof(int))); + Assert.False(converter.CanConvertFrom(typeof(bool))); + Assert.True(converter.CanConvertTo(typeof(string))); + Assert.False(converter.CanConvertTo(typeof(int))); + Assert.False(converter.CanConvertTo(typeof(bool))); + } + + [Fact] + public void PathStringStaysEqualAfterAssignments() + { + PathString p1 = "/?"; + string s1 = p1; + PathString p2 = s1; + Assert.Equal(p1, p2); + } + } +} diff --git a/src/Http/Http.Abstractions/test/QueryStringTests.cs b/src/Http/Http.Abstractions/test/QueryStringTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..8327f12509ac5cd6909aa5b1d729f128137b9227 --- /dev/null +++ b/src/Http/Http.Abstractions/test/QueryStringTests.cs @@ -0,0 +1,166 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Abstractions +{ + public class QueryStringTests + { + [Fact] + public void CtorThrows_IfQueryDoesNotHaveLeadingQuestionMark() + { + // Act and Assert + ExceptionAssert.ThrowsArgument(() => new QueryString("hello"), "value", "The leading '?' must be included for a non-empty query."); + } + + [Fact] + public void CtorNullOrEmpty_Success() + { + var query = new QueryString(); + Assert.False(query.HasValue); + Assert.Null(query.Value); + + query = new QueryString(null); + Assert.False(query.HasValue); + Assert.Null(query.Value); + + query = new QueryString(string.Empty); + Assert.False(query.HasValue); + Assert.Equal(string.Empty, query.Value); + } + + [Fact] + public void CtorJustAQuestionMark_Success() + { + var query = new QueryString("?"); + Assert.True(query.HasValue); + Assert.Equal("?", query.Value); + } + + [Fact] + public void ToString_EncodesHash() + { + var query = new QueryString("?Hello=Wor#ld"); + Assert.Equal("?Hello=Wor%23ld", query.ToString()); + } + + [Theory] + [InlineData("name", "value", "?name=value")] + [InlineData("na me", "val ue", "?na%20me=val%20ue")] + [InlineData("name", "", "?name=")] + [InlineData("name", null, "?name=")] + [InlineData("", "value", "?=value")] + [InlineData("", "", "?=")] + [InlineData("", null, "?=")] + public void CreateNameValue_Success(string name, string value, string exepcted) + { + var query = QueryString.Create(name, value); + Assert.Equal(exepcted, query.Value); + } + + [Fact] + public void CreateFromList_Success() + { + var query = QueryString.Create(new[] + { + new KeyValuePair<string, string>("key1", "value1"), + new KeyValuePair<string, string>("key2", "value2"), + new KeyValuePair<string, string>("key3", "value3"), + new KeyValuePair<string, string>("key4", null), + new KeyValuePair<string, string>("key5", "") + }); + Assert.Equal("?key1=value1&key2=value2&key3=value3&key4=&key5=", query.Value); + } + + [Fact] + public void CreateFromListStringValues_Success() + { + var query = QueryString.Create(new[] + { + new KeyValuePair<string, StringValues>("key1", new StringValues("value1")), + new KeyValuePair<string, StringValues>("key2", new StringValues("value2")), + new KeyValuePair<string, StringValues>("key3", new StringValues("value3")), + new KeyValuePair<string, StringValues>("key4", new StringValues()), + new KeyValuePair<string, StringValues>("key5", new StringValues("")), + }); + Assert.Equal("?key1=value1&key2=value2&key3=value3&key4=&key5=", query.Value); + } + + [Theory] + [InlineData(null, null, null)] + [InlineData("", "", "")] + [InlineData(null, "?name2=value2", "?name2=value2")] + [InlineData("", "?name2=value2", "?name2=value2")] + [InlineData("?", "?name2=value2", "?name2=value2")] + [InlineData("?name1=value1", null, "?name1=value1")] + [InlineData("?name1=value1", "", "?name1=value1")] + [InlineData("?name1=value1", "?", "?name1=value1")] + [InlineData("?name1=value1", "?name2=value2", "?name1=value1&name2=value2")] + public void AddQueryString_Success(string query1, string query2, string expected) + { + var q1 = new QueryString(query1); + var q2 = new QueryString(query2); + Assert.Equal(expected, q1.Add(q2).Value); + Assert.Equal(expected, (q1 + q2).Value); + } + + [Theory] + [InlineData("", "", "", "?=")] + [InlineData("", "", null, "?=")] + [InlineData("?", "", "", "?=")] + [InlineData("?", "", null, "?=")] + [InlineData("?", "name2", "value2", "?name2=value2")] + [InlineData("?", "name2", "", "?name2=")] + [InlineData("?", "name2", null, "?name2=")] + [InlineData("?name1=value1", "name2", "value2", "?name1=value1&name2=value2")] + [InlineData("?name1=value1", "na me2", "val ue2", "?name1=value1&na%20me2=val%20ue2")] + [InlineData("?name1=value1", "", "", "?name1=value1&=")] + [InlineData("?name1=value1", "", null, "?name1=value1&=")] + [InlineData("?name1=value1", "name2", "", "?name1=value1&name2=")] + [InlineData("?name1=value1", "name2", null, "?name1=value1&name2=")] + public void AddNameValue_Success(string query1, string name2, string value2, string expected) + { + var q1 = new QueryString(query1); + var q2 = q1.Add(name2, value2); + Assert.Equal(expected, q2.Value); + } + + [Fact] + public void Equals_EmptyQueryStringAndDefaultQueryString() + { + // Act and Assert + Assert.Equal(default(QueryString), QueryString.Empty); + Assert.Equal(default(QueryString), QueryString.Empty); + // explicitly checking == operator + Assert.True(QueryString.Empty == default(QueryString)); + Assert.True(default(QueryString) == QueryString.Empty); + } + + [Fact] + public void NotEquals_DefaultQueryStringAndNonNullQueryString() + { + // Arrange + var queryString = new QueryString("?foo=1"); + + // Act and Assert + Assert.NotEqual(default(QueryString), queryString); + } + + [Fact] + public void NotEquals_EmptyQueryStringAndNonNullQueryString() + { + // Arrange + var queryString = new QueryString("?foo=1"); + + // Act and Assert + Assert.NotEqual(queryString, QueryString.Empty); + } + } +} diff --git a/src/Http/Http.Abstractions/test/UseMiddlewareTest.cs b/src/Http/Http.Abstractions/test/UseMiddlewareTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..07c1aa4e8d7dc20becd07bff7f01dbebf67ddd63 --- /dev/null +++ b/src/Http/Http.Abstractions/test/UseMiddlewareTest.cs @@ -0,0 +1,376 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Http.Abstractions; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class UseMiddlewareTest + { + [Fact] + public void UseMiddleware_WithNoParameters_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareNoParametersStub)); + var exception = Assert.Throws<InvalidOperationException>(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNoParameters( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, + nameof(HttpContext)), + exception.Message); + } + + [Fact] + public void UseMiddleware_AsyncWithNoParameters_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareAsyncNoParametersStub)); + var exception = Assert.Throws<InvalidOperationException>(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNoParameters( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, + nameof(HttpContext)), + exception.Message); + } + + [Fact] + public void UseMiddleware_NonTaskReturnType_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareNonTaskReturnStub)); + var exception = Assert.Throws<InvalidOperationException>(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNonTaskReturnType( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, + nameof(Task)), + exception.Message); + } + + [Fact] + public void UseMiddleware_AsyncNonTaskReturnType_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareAsyncNonTaskReturnStub)); + var exception = Assert.Throws<InvalidOperationException>(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNonTaskReturnType( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, + nameof(Task)), + exception.Message); + } + + [Fact] + public void UseMiddleware_NoInvokeOrInvokeAsyncMethod_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareNoInvokeStub)); + var exception = Assert.Throws<InvalidOperationException>(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNoInvokeMethod( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, typeof(MiddlewareNoInvokeStub)), + exception.Message); + } + + [Fact] + public void UseMiddleware_MutlipleInvokeMethods_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareMultipleInvokesStub)); + var exception = Assert.Throws<InvalidOperationException>(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddleMutlipleInvokes( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName), + exception.Message); + } + + [Fact] + public void UseMiddleware_MutlipleInvokeAsyncMethods_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareMultipleInvokeAsyncStub)); + var exception = Assert.Throws<InvalidOperationException>(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddleMutlipleInvokes( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName), + exception.Message); + } + + [Fact] + public void UseMiddleware_MutlipleInvokeAndInvokeAsyncMethods_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareMultipleInvokeAndInvokeAsyncStub)); + var exception = Assert.Throws<InvalidOperationException>(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddleMutlipleInvokes( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName), + exception.Message); + } + + [Fact] + public async Task UseMiddleware_ThrowsIfArgCantBeResolvedFromContainer() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareInjectInvokeNoService)); + var app = builder.Build(); + var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => app(new DefaultHttpContext())); + Assert.Equal( + Resources.FormatException_InvokeMiddlewareNoService( + typeof(object), + typeof(MiddlewareInjectInvokeNoService)), + exception.Message); + } + + [Fact] + public void UseMiddlewareWithInvokeArg() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareInjectInvoke)); + var app = builder.Build(); + app(new DefaultHttpContext()); + } + + [Fact] + public void UseMiddlewareWithIvokeWithOutAndRefThrows() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + builder.UseMiddleware(typeof(MiddlewareInjectWithOutAndRefParams)); + var exception = Assert.Throws<NotSupportedException>(() => builder.Build()); + } + + [Fact] + public void UseMiddlewareWithIMiddlewareThrowsIfParametersSpecified() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + var exception = Assert.Throws<NotSupportedException>(() => builder.UseMiddleware(typeof(Middleware), "arg")); + Assert.Equal(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)), exception.Message); + } + + [Fact] + public async Task UseMiddlewareWithIMiddlewareThrowsIfNoIMiddlewareFactoryRegistered() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + builder.UseMiddleware(typeof(Middleware)); + var app = builder.Build(); + var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => + { + var context = new DefaultHttpContext(); + var sp = new DummyServiceProvider(); + context.RequestServices = sp; + await app(context); + }); + Assert.Equal(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory)), exception.Message); + } + + [Fact] + public async Task UseMiddlewareWithIMiddlewareThrowsIfMiddlewareFactoryCreateReturnsNull() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + builder.UseMiddleware(typeof(Middleware)); + var app = builder.Build(); + var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => + { + var context = new DefaultHttpContext(); + var sp = new DummyServiceProvider(); + sp.AddService(typeof(IMiddlewareFactory), new BadMiddlewareFactory()); + context.RequestServices = sp; + await app(context); + }); + + Assert.Equal( + Resources.FormatException_UseMiddlewareUnableToCreateMiddleware( + typeof(BadMiddlewareFactory), + typeof(Middleware)), + exception.Message); + } + + [Fact] + public async Task UseMiddlewareWithIMiddlewareWorks() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + builder.UseMiddleware(typeof(Middleware)); + var app = builder.Build(); + var context = new DefaultHttpContext(); + var sp = new DummyServiceProvider(); + var middlewareFactory = new BasicMiddlewareFactory(); + sp.AddService(typeof(IMiddlewareFactory), middlewareFactory); + context.RequestServices = sp; + await app(context); + Assert.True(Assert.IsType<bool>(context.Items["before"])); + Assert.True(Assert.IsType<bool>(context.Items["after"])); + Assert.NotNull(middlewareFactory.Created); + Assert.NotNull(middlewareFactory.Released); + Assert.IsType<Middleware>(middlewareFactory.Created); + Assert.IsType<Middleware>(middlewareFactory.Released); + Assert.Same(middlewareFactory.Created, middlewareFactory.Released); + } + + public class Middleware : IMiddleware + { + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + context.Items["before"] = true; + await next(context); + context.Items["after"] = true; + } + } + + public class BasicMiddlewareFactory : IMiddlewareFactory + { + public IMiddleware Created { get; private set; } + public IMiddleware Released { get; private set; } + + public IMiddleware Create(Type middlewareType) + { + Created = Activator.CreateInstance(middlewareType) as IMiddleware; + return Created; + } + + public void Release(IMiddleware middleware) + { + Released = middleware; + } + } + + public class BadMiddlewareFactory : IMiddlewareFactory + { + public IMiddleware Create(Type middlewareType) => null; + + public void Release(IMiddleware middleware) { } + } + + private class DummyServiceProvider : IServiceProvider + { + private Dictionary<Type, object> _services = new Dictionary<Type, object>(); + + public void AddService(Type type, object value) => _services[type] = value; + + public object GetService(Type serviceType) + { + if (serviceType == typeof(IServiceProvider)) + { + return this; + } + + if (_services.TryGetValue(serviceType, out object value)) + { + return value; + } + return null; + } + } + + public class MiddlewareInjectWithOutAndRefParams + { + public MiddlewareInjectWithOutAndRefParams(RequestDelegate next) { } + + public Task Invoke(HttpContext context, ref IServiceProvider sp1, out IServiceProvider sp2) + { + sp1 = null; + sp2 = null; + return Task.FromResult(0); + } + } + + private class MiddlewareInjectInvokeNoService + { + public MiddlewareInjectInvokeNoService(RequestDelegate next) { } + + public Task Invoke(HttpContext context, object value) => Task.CompletedTask; + } + + private class MiddlewareInjectInvoke + { + public MiddlewareInjectInvoke(RequestDelegate next) { } + + public Task Invoke(HttpContext context, IServiceProvider provider) => Task.CompletedTask; + } + + private class MiddlewareNoParametersStub + { + public MiddlewareNoParametersStub(RequestDelegate next) { } + + public Task Invoke() => Task.CompletedTask; + } + + private class MiddlewareAsyncNoParametersStub + { + public MiddlewareAsyncNoParametersStub(RequestDelegate next) { } + + public Task InvokeAsync() => Task.CompletedTask; + } + + private class MiddlewareNonTaskReturnStub + { + public MiddlewareNonTaskReturnStub(RequestDelegate next) { } + + public int Invoke() => 0; + } + + private class MiddlewareAsyncNonTaskReturnStub + { + public MiddlewareAsyncNonTaskReturnStub(RequestDelegate next) { } + + public int InvokeAsync() => 0; + } + + private class MiddlewareNoInvokeStub + { + public MiddlewareNoInvokeStub(RequestDelegate next) { } + } + + private class MiddlewareMultipleInvokesStub + { + public MiddlewareMultipleInvokesStub(RequestDelegate next) { } + + public Task Invoke(HttpContext context) => Task.CompletedTask; + + public Task Invoke(HttpContext context, int i) => Task.CompletedTask; + } + + private class MiddlewareMultipleInvokeAsyncStub + { + public MiddlewareMultipleInvokeAsyncStub(RequestDelegate next) { } + + public Task InvokeAsync(HttpContext context) => Task.CompletedTask; + + public Task InvokeAsync(HttpContext context, int i) => Task.CompletedTask; + } + + private class MiddlewareMultipleInvokeAndInvokeAsyncStub + { + public MiddlewareMultipleInvokeAndInvokeAsyncStub(RequestDelegate next) { } + + public Task Invoke(HttpContext context) => Task.CompletedTask; + + public Task InvokeAsync(HttpContext context) => Task.CompletedTask; + } + } +} diff --git a/src/Http/Http.Abstractions/test/UsePathBaseExtensionsTests.cs b/src/Http/Http.Abstractions/test/UsePathBaseExtensionsTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..4b8e9d71cb5e23fe8a5f564ab45d48a36504cb2f --- /dev/null +++ b/src/Http/Http.Abstractions/test/UsePathBaseExtensionsTests.cs @@ -0,0 +1,168 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Builder.Extensions +{ + public class UsePathBaseExtensionsTests + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("/")] + public void EmptyOrNullPathBase_DoNotAddMiddleware(string pathBase) + { + // Arrange + var useCalled = false; + var builder = new ApplicationBuilderWrapper(CreateBuilder(), () => useCalled = true) + .UsePathBase(pathBase); + + // Act + builder.Build(); + + // Assert + Assert.False(useCalled); + } + + private class ApplicationBuilderWrapper : IApplicationBuilder + { + private readonly IApplicationBuilder _wrappedBuilder; + private readonly Action _useCallback; + + public ApplicationBuilderWrapper(IApplicationBuilder applicationBuilder, Action useCallback) + { + _wrappedBuilder = applicationBuilder; + _useCallback = useCallback; + } + + public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware) + { + _useCallback(); + return _wrappedBuilder.Use(middleware); + } + + public IServiceProvider ApplicationServices + { + get { return _wrappedBuilder.ApplicationServices; } + set { _wrappedBuilder.ApplicationServices = value; } + } + + public IDictionary<string, object> Properties => _wrappedBuilder.Properties; + public IFeatureCollection ServerFeatures => _wrappedBuilder.ServerFeatures; + public RequestDelegate Build() => _wrappedBuilder.Build(); + public IApplicationBuilder New() => _wrappedBuilder.New(); + + } + + [Theory] + [InlineData("/base", "", "/base", "/base", "")] + [InlineData("/base", "", "/base/", "/base", "/")] + [InlineData("/base", "", "/base/something", "/base", "/something")] + [InlineData("/base", "", "/base/something/", "/base", "/something/")] + [InlineData("/base/more", "", "/base/more", "/base/more", "")] + [InlineData("/base/more", "", "/base/more/something", "/base/more", "/something")] + [InlineData("/base/more", "", "/base/more/something/", "/base/more", "/something/")] + [InlineData("/base", "/oldbase", "/base", "/oldbase/base", "")] + [InlineData("/base", "/oldbase", "/base/", "/oldbase/base", "/")] + [InlineData("/base", "/oldbase", "/base/something", "/oldbase/base", "/something")] + [InlineData("/base", "/oldbase", "/base/something/", "/oldbase/base", "/something/")] + [InlineData("/base/more", "/oldbase", "/base/more", "/oldbase/base/more", "")] + [InlineData("/base/more", "/oldbase", "/base/more/something", "/oldbase/base/more", "/something")] + [InlineData("/base/more", "/oldbase", "/base/more/something/", "/oldbase/base/more", "/something/")] + public void RequestPathBaseContainingPathBase_IsSplit(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + [Theory] + [InlineData("/base", "", "/something", "", "/something")] + [InlineData("/base", "", "/baseandsomething", "", "/baseandsomething")] + [InlineData("/base", "", "/ba", "", "/ba")] + [InlineData("/base", "", "/ba/se", "", "/ba/se")] + [InlineData("/base", "/oldbase", "/something", "/oldbase", "/something")] + [InlineData("/base", "/oldbase", "/baseandsomething", "/oldbase", "/baseandsomething")] + [InlineData("/base", "/oldbase", "/ba", "/oldbase", "/ba")] + [InlineData("/base", "/oldbase", "/ba/se", "/oldbase", "/ba/se")] + public void RequestPathBaseNotContainingPathBase_IsNotSplit(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + [Theory] + [InlineData("", "", "/", "", "/")] + [InlineData("/", "", "/", "", "/")] + [InlineData("/base", "", "/base/", "/base", "/")] + [InlineData("/base/", "", "/base", "/base", "")] + [InlineData("/base/", "", "/base/", "/base", "/")] + [InlineData("", "/oldbase", "/", "/oldbase", "/")] + [InlineData("/", "/oldbase", "/", "/oldbase", "/")] + [InlineData("/base", "/oldbase", "/base/", "/oldbase/base", "/")] + [InlineData("/base/", "/oldbase", "/base", "/oldbase/base", "")] + [InlineData("/base/", "/oldbase", "/base/", "/oldbase/base", "/")] + public void PathBaseNeverEndsWithSlash(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + [Theory] + [InlineData("/base", "", "/Base/Something", "/Base", "/Something")] + [InlineData("/base", "/OldBase", "/Base/Something", "/OldBase/Base", "/Something")] + public void PathBaseAndPathPreserveRequestCasing(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + [Theory] + [InlineData("/b♫se", "", "/b♫se/something", "/b♫se", "/something")] + [InlineData("/b♫se", "", "/B♫se/something", "/B♫se", "/something")] + [InlineData("/b♫se", "", "/b♫se/Something", "/b♫se", "/Something")] + [InlineData("/b♫se", "/oldb♫se", "/b♫se/something", "/oldb♫se/b♫se", "/something")] + [InlineData("/b♫se", "/oldb♫se", "/b♫se/Something", "/oldb♫se/b♫se", "/Something")] + [InlineData("/b♫se", "/oldb♫se", "/B♫se/something", "/oldb♫se/B♫se", "/something")] + public void PathBaseCanHaveUnicodeCharacters(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + private static void TestPathBase(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + HttpContext requestContext = CreateRequest(pathBase, requestPath); + var builder = CreateBuilder() + .UsePathBase(registeredPathBase); + builder.Run(context => + { + context.Items["test.Path"] = context.Request.Path; + context.Items["test.PathBase"] = context.Request.PathBase; + return Task.FromResult(0); + }); + builder.Build().Invoke(requestContext).Wait(); + + // Assert path and pathBase are split after middleware + Assert.Equal(expectedPath, ((PathString)requestContext.Items["test.Path"]).Value); + Assert.Equal(expectedPathBase, ((PathString)requestContext.Items["test.PathBase"]).Value); + // Assert path and pathBase are reset after request + Assert.Equal(pathBase, requestContext.Request.PathBase.Value); + Assert.Equal(requestPath, requestContext.Request.Path.Value); + } + + private static HttpContext CreateRequest(string pathBase, string requestPath) + { + HttpContext context = new DefaultHttpContext(); + context.Request.PathBase = new PathString(pathBase); + context.Request.Path = new PathString(requestPath); + return context; + } + + private static ApplicationBuilder CreateBuilder() + { + return new ApplicationBuilder(serviceProvider: null); + } + } +} diff --git a/src/Http/Http.Abstractions/test/UseWhenExtensionsTests.cs b/src/Http/Http.Abstractions/test/UseWhenExtensionsTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..902a003b26dac1036f9270ce51dcdca6b62619ec --- /dev/null +++ b/src/Http/Http.Abstractions/test/UseWhenExtensionsTests.cs @@ -0,0 +1,170 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Builder.Extensions +{ + public class UseWhenExtensionsTests + { + [Fact] + public void NullArguments_ArgumentNullException() + { + // Arrange + var builder = CreateBuilder(); + + // Act + Action nullPredicate = () => builder.UseWhen(null, app => { }); + Action nullConfiguration = () => builder.UseWhen(TruePredicate, null); + + // Assert + Assert.Throws<ArgumentNullException>(nullPredicate); + Assert.Throws<ArgumentNullException>(nullConfiguration); + } + + [Fact] + public void PredicateTrue_BranchTaken_WillRejoin() + { + // Arrange + var context = CreateContext(); + var parent = CreateBuilder(); + + parent.UseWhen(TruePredicate, child => + { + child.UseWhen(TruePredicate, grandchild => + { + grandchild.Use(Increment("grandchild")); + }); + + child.Use(Increment("child")); + }); + + parent.Use(Increment("parent")); + + // Act + parent.Build().Invoke(context).Wait(); + + // Assert + Assert.Equal(1, Count(context, "parent")); + Assert.Equal(1, Count(context, "child")); + Assert.Equal(1, Count(context, "grandchild")); + } + + [Fact] + public void PredicateTrue_BranchTaken_CanTerminate() + { + // Arrange + var context = CreateContext(); + var parent = CreateBuilder(); + + parent.UseWhen(TruePredicate, child => + { + child.UseWhen(TruePredicate, grandchild => + { + grandchild.Use(Increment("grandchild", terminate: true)); + }); + + child.Use(Increment("child")); + }); + + parent.Use(Increment("parent")); + + // Act + parent.Build().Invoke(context).Wait(); + + // Assert + Assert.Equal(0, Count(context, "parent")); + Assert.Equal(0, Count(context, "child")); + Assert.Equal(1, Count(context, "grandchild")); + } + + [Fact] + public void PredicateFalse_PassThrough() + { + // Arrange + var context = CreateContext(); + var parent = CreateBuilder(); + + parent.UseWhen(FalsePredicate, child => + { + child.Use(Increment("child")); + }); + + parent.Use(Increment("parent")); + + // Act + parent.Build().Invoke(context).Wait(); + + // Assert + Assert.Equal(1, Count(context, "parent")); + Assert.Equal(0, Count(context, "child")); + } + + private static HttpContext CreateContext() + { + return new DefaultHttpContext(); + } + + private static ApplicationBuilder CreateBuilder() + { + return new ApplicationBuilder(serviceProvider: null); + } + + private static bool TruePredicate(HttpContext context) + { + return true; + } + + private static bool FalsePredicate(HttpContext context) + { + return false; + } + + private static Func<HttpContext, Func<Task>, Task> Increment(string key, bool terminate = false) + { + return (context, next) => + { + if (!context.Items.ContainsKey(key)) + { + context.Items[key] = 1; + } + else + { + var item = context.Items[key]; + + if (item is int) + { + context.Items[key] = 1 + (int)item; + } + else + { + context.Items[key] = 1; + } + } + + return terminate ? Task.FromResult<object>(null) : next(); + }; + } + + private static int Count(HttpContext context, string key) + { + if (!context.Items.ContainsKey(key)) + { + return 0; + } + + var item = context.Items[key]; + + if (item is int) + { + return (int)item; + } + + return 0; + } + } +} diff --git a/src/Http/Http.Extensions/src/HeaderDictionaryTypeExtensions.cs b/src/Http/Http.Extensions/src/HeaderDictionaryTypeExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..1723ee6fd5c032cda4b197484f1daf7c57862f28 --- /dev/null +++ b/src/Http/Http.Extensions/src/HeaderDictionaryTypeExtensions.cs @@ -0,0 +1,287 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Http.Headers; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http +{ + public static class HeaderDictionaryTypeExtensions + { + public static RequestHeaders GetTypedHeaders(this HttpRequest request) + { + return new RequestHeaders(request.Headers); + } + + public static ResponseHeaders GetTypedHeaders(this HttpResponse response) + { + return new ResponseHeaders(response.Headers); + } + + // These are all shared helpers used by both RequestHeaders and ResponseHeaders + + internal static DateTimeOffset? GetDate(this IHeaderDictionary headers, string name) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return headers.Get<DateTimeOffset?>(name); + } + + internal static void Set(this IHeaderDictionary headers, string name, object value) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value == null) + { + headers.Remove(name); + } + else + { + headers[name] = value.ToString(); + } + } + + internal static void SetList<T>(this IHeaderDictionary headers, string name, IList<T> values) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (values == null || values.Count == 0) + { + headers.Remove(name); + } + else if (values.Count == 1) + { + headers[name] = new StringValues(values[0].ToString()); + } + else + { + var newValues = new string[values.Count]; + for (var i = 0; i < values.Count; i++) + { + newValues[i] = values[i].ToString(); + } + headers[name] = new StringValues(newValues); + } + } + + public static void AppendList<T>(this IHeaderDictionary Headers, string name, IList<T> values) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + switch (values.Count) + { + case 0: + Headers.Append(name, StringValues.Empty); + break; + case 1: + Headers.Append(name, new StringValues(values[0].ToString())); + break; + default: + var newValues = new string[values.Count]; + for (var i = 0; i < values.Count; i++) + { + newValues[i] = values[i].ToString(); + } + Headers.Append(name, new StringValues(newValues)); + break; + } + } + + internal static void SetDate(this IHeaderDictionary headers, string name, DateTimeOffset? value) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value.HasValue) + { + headers[name] = HeaderUtilities.FormatDate(value.Value); + } + else + { + headers.Remove(name); + } + } + + private static IDictionary<Type, object> KnownParsers = new Dictionary<Type, object>() + { + { typeof(CacheControlHeaderValue), new Func<string, CacheControlHeaderValue>(value => { CacheControlHeaderValue result; return CacheControlHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(ContentDispositionHeaderValue), new Func<string, ContentDispositionHeaderValue>(value => { ContentDispositionHeaderValue result; return ContentDispositionHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(ContentRangeHeaderValue), new Func<string, ContentRangeHeaderValue>(value => { ContentRangeHeaderValue result; return ContentRangeHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(MediaTypeHeaderValue), new Func<string, MediaTypeHeaderValue>(value => { MediaTypeHeaderValue result; return MediaTypeHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(RangeConditionHeaderValue), new Func<string, RangeConditionHeaderValue>(value => { RangeConditionHeaderValue result; return RangeConditionHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(RangeHeaderValue), new Func<string, RangeHeaderValue>(value => { RangeHeaderValue result; return RangeHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(EntityTagHeaderValue), new Func<string, EntityTagHeaderValue>(value => { EntityTagHeaderValue result; return EntityTagHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(DateTimeOffset?), new Func<string, DateTimeOffset?>(value => { DateTimeOffset result; return HeaderUtilities.TryParseDate(value, out result) ? result : (DateTimeOffset?)null; }) }, + { typeof(long?), new Func<string, long?>(value => { long result; return HeaderUtilities.TryParseNonNegativeInt64(value, out result) ? result : (long?)null; }) }, + }; + + private static IDictionary<Type, object> KnownListParsers = new Dictionary<Type, object>() + { + { typeof(MediaTypeHeaderValue), new Func<IList<string>, IList<MediaTypeHeaderValue>>(value => { IList<MediaTypeHeaderValue> result; return MediaTypeHeaderValue.TryParseList(value, out result) ? result : null; }) }, + { typeof(StringWithQualityHeaderValue), new Func<IList<string>, IList<StringWithQualityHeaderValue>>(value => { IList<StringWithQualityHeaderValue> result; return StringWithQualityHeaderValue.TryParseList(value, out result) ? result : null; }) }, + { typeof(CookieHeaderValue), new Func<IList<string>, IList<CookieHeaderValue>>(value => { IList<CookieHeaderValue> result; return CookieHeaderValue.TryParseList(value, out result) ? result : null; }) }, + { typeof(EntityTagHeaderValue), new Func<IList<string>, IList<EntityTagHeaderValue>>(value => { IList<EntityTagHeaderValue> result; return EntityTagHeaderValue.TryParseList(value, out result) ? result : null; }) }, + { typeof(SetCookieHeaderValue), new Func<IList<string>, IList<SetCookieHeaderValue>>(value => { IList<SetCookieHeaderValue> result; return SetCookieHeaderValue.TryParseList(value, out result) ? result : null; }) }, + }; + + internal static T Get<T>(this IHeaderDictionary headers, string name) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + object temp; + var value = headers[name]; + + if (StringValues.IsNullOrEmpty(value)) + { + return default(T); + } + + if (KnownParsers.TryGetValue(typeof(T), out temp)) + { + var func = (Func<string, T>)temp; + return func(value); + } + + return GetViaReflection<T>(value.ToString()); + } + + internal static IList<T> GetList<T>(this IHeaderDictionary headers, string name) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + object temp; + var values = headers[name]; + + if (StringValues.IsNullOrEmpty(values)) + { + return null; + } + + if (KnownListParsers.TryGetValue(typeof(T), out temp)) + { + var func = (Func<IList<string>, IList<T>>)temp; + return func(values); + } + + return GetListViaReflection<T>(values); + } + + private static T GetViaReflection<T>(string value) + { + // TODO: Cache the reflected type for later? Only if success? + var type = typeof(T); + var method = type.GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(methodInfo => + { + if (string.Equals("TryParse", methodInfo.Name, StringComparison.Ordinal) + && methodInfo.ReturnParameter.ParameterType.Equals(typeof(bool))) + { + var methodParams = methodInfo.GetParameters(); + return methodParams.Length == 2 + && methodParams[0].ParameterType.Equals(typeof(string)) + && methodParams[1].IsOut + && methodParams[1].ParameterType.Equals(type.MakeByRefType()); + } + return false; + }); + + if (method == null) + { + throw new NotSupportedException(string.Format( + "The given type '{0}' does not have a TryParse method with the required signature 'public static bool TryParse(string, out {0}).", nameof(T))); + } + + var parameters = new object[] { value, null }; + var success = (bool)method.Invoke(null, parameters); + if (success) + { + return (T)parameters[1]; + } + return default(T); + } + + private static IList<T> GetListViaReflection<T>(StringValues values) + { + // TODO: Cache the reflected type for later? Only if success? + var type = typeof(T); + var method = type.GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(methodInfo => + { + if (string.Equals("TryParseList", methodInfo.Name, StringComparison.Ordinal) + && methodInfo.ReturnParameter.ParameterType.Equals(typeof(Boolean))) + { + var methodParams = methodInfo.GetParameters(); + return methodParams.Length == 2 + && methodParams[0].ParameterType.Equals(typeof(IList<string>)) + && methodParams[1].IsOut + && methodParams[1].ParameterType.Equals(typeof(IList<T>).MakeByRefType()); + } + return false; + }); + + if (method == null) + { + throw new NotSupportedException(string.Format( + "The given type '{0}' does not have a TryParseList method with the required signature 'public static bool TryParseList(IList<string>, out IList<{0}>).", nameof(T))); + } + + var parameters = new object[] { values, null }; + var success = (bool)method.Invoke(null, parameters); + if (success) + { + return (IList<T>)parameters[1]; + } + return null; + } + } +} diff --git a/src/Http/Http.Extensions/src/HttpRequestMultipartExtensions.cs b/src/Http/Http.Extensions/src/HttpRequestMultipartExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..da9188dad37d7721ed40aefb2007f3f954ddd8e5 --- /dev/null +++ b/src/Http/Http.Extensions/src/HttpRequestMultipartExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Extensions +{ + public static class HttpRequestMultipartExtensions + { + public static string GetMultipartBoundary(this HttpRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + MediaTypeHeaderValue mediaType; + if (!MediaTypeHeaderValue.TryParse(request.ContentType, out mediaType)) + { + return string.Empty; + } + return HeaderUtilities.RemoveQuotes(mediaType.Boundary).ToString(); + } + } +} diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj new file mode 100644 index 0000000000000000000000000000000000000000..25ae2af17a7757357ba29555dba51984b7ab2482 --- /dev/null +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -0,0 +1,18 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>ASP.NET Core common extension methods for HTTP abstractions, HTTP headers, HTTP request/response, and session state.</Description> + <TargetFramework>netstandard2.0</TargetFramework> + <NoWarn>$(NoWarn);CS1591</NoWarn> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore</PackageTags> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Http.Abstractions" /> + <Reference Include="Microsoft.Net.Http.Headers" /> + <Reference Include="Microsoft.Extensions.FileProviders.Abstractions" /> + <Reference Include="System.Buffers" /> + </ItemGroup> + +</Project> diff --git a/src/Http/Http.Extensions/src/QueryBuilder.cs b/src/Http/Http.Extensions/src/QueryBuilder.cs new file mode 100644 index 0000000000000000000000000000000000000000..e9feb391b17b7dc664a4478d43413c3a5f049ccc --- /dev/null +++ b/src/Http/Http.Extensions/src/QueryBuilder.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Text.Encodings.Web; + +namespace Microsoft.AspNetCore.Http.Extensions +{ + // The IEnumerable interface is required for the collection initialization syntax: new QueryBuilder() { { "key", "value" } }; + public class QueryBuilder : IEnumerable<KeyValuePair<string, string>> + { + private IList<KeyValuePair<string, string>> _params; + + public QueryBuilder() + { + _params = new List<KeyValuePair<string, string>>(); + } + + public QueryBuilder(IEnumerable<KeyValuePair<string, string>> parameters) + { + _params = new List<KeyValuePair<string, string>>(parameters); + } + + public void Add(string key, IEnumerable<string> values) + { + foreach (var value in values) + { + _params.Add(new KeyValuePair<string, string>(key, value)); + } + } + + public void Add(string key, string value) + { + _params.Add(new KeyValuePair<string, string>(key, value)); + } + + public override string ToString() + { + var builder = new StringBuilder(); + bool first = true; + for (int i = 0; i < _params.Count; i++) + { + var pair = _params[i]; + builder.Append(first ? "?" : "&"); + first = false; + builder.Append(UrlEncoder.Default.Encode(pair.Key)); + builder.Append("="); + builder.Append(UrlEncoder.Default.Encode(pair.Value)); + } + + return builder.ToString(); + } + + public QueryString ToQueryString() + { + return new QueryString(ToString()); + } + + public override int GetHashCode() + { + return ToQueryString().GetHashCode(); + } + + public override bool Equals(object obj) + { + return ToQueryString().Equals(obj); + } + + public IEnumerator<KeyValuePair<string, string>> GetEnumerator() + { + return _params.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _params.GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/RequestHeaders.cs b/src/Http/Http.Extensions/src/RequestHeaders.cs new file mode 100644 index 0000000000000000000000000000000000000000..12246922d43aa7eb73e9094c809ac93229dd64cf --- /dev/null +++ b/src/Http/Http.Extensions/src/RequestHeaders.cs @@ -0,0 +1,332 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Headers +{ + public class RequestHeaders + { + public RequestHeaders(IHeaderDictionary headers) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + Headers = headers; + } + + public IHeaderDictionary Headers { get; } + + public IList<MediaTypeHeaderValue> Accept + { + get + { + return Headers.GetList<MediaTypeHeaderValue>(HeaderNames.Accept); + } + set + { + Headers.SetList(HeaderNames.Accept, value); + } + } + + public IList<StringWithQualityHeaderValue> AcceptCharset + { + get + { + return Headers.GetList<StringWithQualityHeaderValue>(HeaderNames.AcceptCharset); + } + set + { + Headers.SetList(HeaderNames.AcceptCharset, value); + } + } + + public IList<StringWithQualityHeaderValue> AcceptEncoding + { + get + { + return Headers.GetList<StringWithQualityHeaderValue>(HeaderNames.AcceptEncoding); + } + set + { + Headers.SetList(HeaderNames.AcceptEncoding, value); + } + } + + public IList<StringWithQualityHeaderValue> AcceptLanguage + { + get + { + return Headers.GetList<StringWithQualityHeaderValue>(HeaderNames.AcceptLanguage); + } + set + { + Headers.SetList(HeaderNames.AcceptLanguage, value); + } + } + + public CacheControlHeaderValue CacheControl + { + get + { + return Headers.Get<CacheControlHeaderValue>(HeaderNames.CacheControl); + } + set + { + Headers.Set(HeaderNames.CacheControl, value); + } + } + + public ContentDispositionHeaderValue ContentDisposition + { + get + { + return Headers.Get<ContentDispositionHeaderValue>(HeaderNames.ContentDisposition); + } + set + { + Headers.Set(HeaderNames.ContentDisposition, value); + } + } + + public long? ContentLength + { + get + { + return Headers.ContentLength; + } + set + { + Headers.ContentLength = value; + } + } + + public ContentRangeHeaderValue ContentRange + { + get + { + return Headers.Get<ContentRangeHeaderValue>(HeaderNames.ContentRange); + } + set + { + Headers.Set(HeaderNames.ContentRange, value); + } + } + + public MediaTypeHeaderValue ContentType + { + get + { + return Headers.Get<MediaTypeHeaderValue>(HeaderNames.ContentType); + } + set + { + Headers.Set(HeaderNames.ContentType, value); + } + } + + public IList<CookieHeaderValue> Cookie + { + get + { + return Headers.GetList<CookieHeaderValue>(HeaderNames.Cookie); + } + set + { + Headers.SetList(HeaderNames.Cookie, value); + } + } + + public DateTimeOffset? Date + { + get + { + return Headers.GetDate(HeaderNames.Date); + } + set + { + Headers.SetDate(HeaderNames.Date, value); + } + } + + public DateTimeOffset? Expires + { + get + { + return Headers.GetDate(HeaderNames.Expires); + } + set + { + Headers.SetDate(HeaderNames.Expires, value); + } + } + + public HostString Host + { + get + { + return HostString.FromUriComponent(Headers[HeaderNames.Host]); + } + set + { + Headers[HeaderNames.Host] = value.ToUriComponent(); + } + } + + public IList<EntityTagHeaderValue> IfMatch + { + get + { + return Headers.GetList<EntityTagHeaderValue>(HeaderNames.IfMatch); + } + set + { + Headers.SetList(HeaderNames.IfMatch, value); + } + } + + public DateTimeOffset? IfModifiedSince + { + get + { + return Headers.GetDate(HeaderNames.IfModifiedSince); + } + set + { + Headers.SetDate(HeaderNames.IfModifiedSince, value); + } + } + + public IList<EntityTagHeaderValue> IfNoneMatch + { + get + { + return Headers.GetList<EntityTagHeaderValue>(HeaderNames.IfNoneMatch); + } + set + { + Headers.SetList(HeaderNames.IfNoneMatch, value); + } + } + + public RangeConditionHeaderValue IfRange + { + get + { + return Headers.Get<RangeConditionHeaderValue>(HeaderNames.IfRange); + } + set + { + Headers.Set(HeaderNames.IfRange, value); + } + } + + public DateTimeOffset? IfUnmodifiedSince + { + get + { + return Headers.GetDate(HeaderNames.IfUnmodifiedSince); + } + set + { + Headers.SetDate(HeaderNames.IfUnmodifiedSince, value); + } + } + + public DateTimeOffset? LastModified + { + get + { + return Headers.GetDate(HeaderNames.LastModified); + } + set + { + Headers.SetDate(HeaderNames.LastModified, value); + } + } + + public RangeHeaderValue Range + { + get + { + return Headers.Get<RangeHeaderValue>(HeaderNames.Range); + } + set + { + Headers.Set(HeaderNames.Range, value); + } + } + + public Uri Referer + { + get + { + Uri uri; + if (Uri.TryCreate(Headers[HeaderNames.Referer], UriKind.RelativeOrAbsolute, out uri)) + { + return uri; + } + return null; + } + set + { + Headers.Set(HeaderNames.Referer, value == null ? null : UriHelper.Encode(value)); + } + } + + public T Get<T>(string name) + { + return Headers.Get<T>(name); + } + + public IList<T> GetList<T>(string name) + { + return Headers.GetList<T>(name); + } + + public void Set(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + Headers.Set(name, value); + } + + public void SetList<T>(string name, IList<T> values) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + Headers.SetList<T>(name, values); + } + + public void Append(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + Headers.Append(name, value.ToString()); + } + + public void AppendList<T>(string name, IList<T> values) + { + Headers.AppendList<T>(name, values); + } + } +} diff --git a/src/Http/Http.Extensions/src/ResponseExtensions.cs b/src/Http/Http.Extensions/src/ResponseExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..6c5d92a7af41c748d84f57ecdbcc556b256a1af2 --- /dev/null +++ b/src/Http/Http.Extensions/src/ResponseExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http +{ + public static class ResponseExtensions + { + public static void Clear(this HttpResponse response) + { + if (response.HasStarted) + { + throw new InvalidOperationException("The response cannot be cleared, it has already started sending."); + } + response.StatusCode = 200; + response.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = null; + response.Headers.Clear(); + if (response.Body.CanSeek) + { + response.Body.SetLength(0); + } + } + } +} diff --git a/src/Http/Http.Extensions/src/ResponseHeaders.cs b/src/Http/Http.Extensions/src/ResponseHeaders.cs new file mode 100644 index 0000000000000000000000000000000000000000..87e3c0318c9564c8ac1b5a0b406af15e79dff2a9 --- /dev/null +++ b/src/Http/Http.Extensions/src/ResponseHeaders.cs @@ -0,0 +1,211 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Headers +{ + public class ResponseHeaders + { + public ResponseHeaders(IHeaderDictionary headers) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + Headers = headers; + } + + public IHeaderDictionary Headers { get; } + + public CacheControlHeaderValue CacheControl + { + get + { + return Headers.Get<CacheControlHeaderValue>(HeaderNames.CacheControl); + } + set + { + Headers.Set(HeaderNames.CacheControl, value); + } + } + + public ContentDispositionHeaderValue ContentDisposition + { + get + { + return Headers.Get<ContentDispositionHeaderValue>(HeaderNames.ContentDisposition); + } + set + { + Headers.Set(HeaderNames.ContentDisposition, value); + } + } + + public long? ContentLength + { + get + { + return Headers.ContentLength; + } + set + { + Headers.ContentLength = value; + } + } + + public ContentRangeHeaderValue ContentRange + { + get + { + return Headers.Get<ContentRangeHeaderValue>(HeaderNames.ContentRange); + } + set + { + Headers.Set(HeaderNames.ContentRange, value); + } + } + + public MediaTypeHeaderValue ContentType + { + get + { + return Headers.Get<MediaTypeHeaderValue>(HeaderNames.ContentType); + } + set + { + Headers.Set(HeaderNames.ContentType, value); + } + } + + public DateTimeOffset? Date + { + get + { + return Headers.GetDate(HeaderNames.Date); + } + set + { + Headers.SetDate(HeaderNames.Date, value); + } + } + + public EntityTagHeaderValue ETag + { + get + { + return Headers.Get<EntityTagHeaderValue>(HeaderNames.ETag); + } + set + { + Headers.Set(HeaderNames.ETag, value); + } + } + public DateTimeOffset? Expires + { + get + { + return Headers.GetDate(HeaderNames.Expires); + } + set + { + Headers.SetDate(HeaderNames.Expires, value); + } + } + + public DateTimeOffset? LastModified + { + get + { + return Headers.GetDate(HeaderNames.LastModified); + } + set + { + Headers.SetDate(HeaderNames.LastModified, value); + } + } + + public Uri Location + { + get + { + Uri uri; + if (Uri.TryCreate(Headers[HeaderNames.Location], UriKind.RelativeOrAbsolute, out uri)) + { + return uri; + } + return null; + } + set + { + Headers.Set(HeaderNames.Location, value == null ? null : UriHelper.Encode(value)); + } + } + + public IList<SetCookieHeaderValue> SetCookie + { + get + { + return Headers.GetList<SetCookieHeaderValue>(HeaderNames.SetCookie); + } + set + { + Headers.SetList(HeaderNames.SetCookie, value); + } + } + + public T Get<T>(string name) + { + return Headers.Get<T>(name); + } + + public IList<T> GetList<T>(string name) + { + return Headers.GetList<T>(name); + } + + public void Set(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + Headers.Set(name, value); + } + + public void SetList<T>(string name, IList<T> values) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + Headers.SetList<T>(name, values); + } + + public void Append(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + Headers.Append(name, value.ToString()); + } + + public void AppendList<T>(string name, IList<T> values) + { + Headers.AppendList<T>(name, values); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs b/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..74c0422ef41f7f20c59721c1764a707e7754d42e --- /dev/null +++ b/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs @@ -0,0 +1,181 @@ +// 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; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Provides extensions for HttpResponse exposing the SendFile extension. + /// </summary> + public static class SendFileResponseExtensions + { + /// <summary> + /// Sends the given file using the SendFile extension. + /// </summary> + /// <param name="response"></param> + /// <param name="file">The file.</param> + /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> + public static Task SendFileAsync(this HttpResponse response, IFileInfo file, CancellationToken cancellationToken = default) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + + return SendFileAsyncCore(response, file, 0, null, cancellationToken); + } + + /// <summary> + /// Sends the given file using the SendFile extension. + /// </summary> + /// <param name="response"></param> + /// <param name="file">The file.</param> + /// <param name="offset">The offset in the file.</param> + /// <param name="count">The number of bytes to send, or null to send the remainder of the file.</param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + public static Task SendFileAsync(this HttpResponse response, IFileInfo file, long offset, long? count, CancellationToken cancellationToken = default) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + + return SendFileAsyncCore(response, file, offset, count, cancellationToken); + } + + /// <summary> + /// Sends the given file using the SendFile extension. + /// </summary> + /// <param name="response"></param> + /// <param name="fileName">The full path to the file.</param> + /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> + /// <returns></returns> + public static Task SendFileAsync(this HttpResponse response, string fileName, CancellationToken cancellationToken = default) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + + return SendFileAsyncCore(response, fileName, 0, null, cancellationToken); + } + + /// <summary> + /// Sends the given file using the SendFile extension. + /// </summary> + /// <param name="response"></param> + /// <param name="fileName">The full path to the file.</param> + /// <param name="offset">The offset in the file.</param> + /// <param name="count">The number of bytes to send, or null to send the remainder of the file.</param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + public static Task SendFileAsync(this HttpResponse response, string fileName, long offset, long? count, CancellationToken cancellationToken = default) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + + return SendFileAsyncCore(response, fileName, offset, count, cancellationToken); + } + + private static async Task SendFileAsyncCore(HttpResponse response, IFileInfo file, long offset, long? count, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(file.PhysicalPath)) + { + CheckRange(offset, count, file.Length); + + using (var fileContent = file.CreateReadStream()) + { + if (offset > 0) + { + fileContent.Seek(offset, SeekOrigin.Begin); + } + await StreamCopyOperation.CopyToAsync(fileContent, response.Body, count, cancellationToken); + } + } + else + { + await response.SendFileAsync(file.PhysicalPath, offset, count, cancellationToken); + } + } + + private static Task SendFileAsyncCore(HttpResponse response, string fileName, long offset, long? count, CancellationToken cancellationToken = default) + { + var sendFile = response.HttpContext.Features.Get<IHttpSendFileFeature>(); + if (sendFile == null) + { + return SendFileAsyncCore(response.Body, fileName, offset, count, cancellationToken); + } + + return sendFile.SendFileAsync(fileName, offset, count, cancellationToken); + } + + // Not safe for overlapped writes. + private static async Task SendFileAsyncCore(Stream outputStream, string fileName, long offset, long? count, CancellationToken cancel = default) + { + cancel.ThrowIfCancellationRequested(); + + var fileInfo = new FileInfo(fileName); + CheckRange(offset, count, fileInfo.Length); + + int bufferSize = 1024 * 16; + var fileStream = new FileStream( + fileName, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + bufferSize: bufferSize, + options: FileOptions.Asynchronous | FileOptions.SequentialScan); + + using (fileStream) + { + if (offset > 0) + { + fileStream.Seek(offset, SeekOrigin.Begin); + } + + await StreamCopyOperation.CopyToAsync(fileStream, outputStream, count, cancel); + } + } + + private static void CheckRange(long offset, long? count, long fileLength) + { + if (offset < 0 || offset > fileLength) + { + throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); + } + if (count.HasValue && + (count.Value < 0 || count.Value > fileLength - offset)) + { + throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/SessionExtensions.cs b/src/Http/Http.Extensions/src/SessionExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..fd7573fa95db91d68b0620c27c48ea1186b162bc --- /dev/null +++ b/src/Http/Http.Extensions/src/SessionExtensions.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text; + +namespace Microsoft.AspNetCore.Http +{ + public static class SessionExtensions + { + public static void SetInt32(this ISession session, string key, int value) + { + var bytes = new byte[] + { + (byte)(value >> 24), + (byte)(0xFF & (value >> 16)), + (byte)(0xFF & (value >> 8)), + (byte)(0xFF & value) + }; + session.Set(key, bytes); + } + + public static int? GetInt32(this ISession session, string key) + { + var data = session.Get(key); + if (data == null || data.Length < 4) + { + return null; + } + return data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]; + } + + public static void SetString(this ISession session, string key, string value) + { + session.Set(key, Encoding.UTF8.GetBytes(value)); + } + + public static string GetString(this ISession session, string key) + { + var data = session.Get(key); + if (data == null) + { + return null; + } + return Encoding.UTF8.GetString(data); + } + + public static byte[] Get(this ISession session, string key) + { + byte[] value = null; + session.TryGetValue(key, out value); + return value; + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/StreamCopyOperation.cs b/src/Http/Http.Extensions/src/StreamCopyOperation.cs new file mode 100644 index 0000000000000000000000000000000000000000..12067fef6518d06713db6500380f7e7d84a51074 --- /dev/null +++ b/src/Http/Http.Extensions/src/StreamCopyOperation.cs @@ -0,0 +1,87 @@ +// 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.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Extensions +{ + // FYI: In most cases the source will be a FileStream and the destination will be to the network. + public static class StreamCopyOperation + { + private const int DefaultBufferSize = 4096; + + /// <summary>Asynchronously reads the bytes from the source stream and writes them to another stream.</summary> + /// <returns>A task that represents the asynchronous copy operation.</returns> + /// <param name="source">The stream from which the contents will be copied.</param> + /// <param name="destination">The stream to which the contents of the current stream will be copied.</param> + /// <param name="count">The count of bytes to be copied.</param> + /// <param name="cancel">The token to monitor for cancellation requests. The default value is <see cref="P:System.Threading.CancellationToken.None" />.</param> + public static Task CopyToAsync(Stream source, Stream destination, long? count, CancellationToken cancel) + { + return CopyToAsync(source, destination, count, DefaultBufferSize, cancel); + } + + /// <summary>Asynchronously reads the bytes from the source stream and writes them to another stream, using a specified buffer size.</summary> + /// <returns>A task that represents the asynchronous copy operation.</returns> + /// <param name="source">The stream from which the contents will be copied.</param> + /// <param name="destination">The stream to which the contents of the current stream will be copied.</param> + /// <param name="count">The count of bytes to be copied.</param> + /// <param name="bufferSize">The size, in bytes, of the buffer. This value must be greater than zero. The default size is 4096.</param> + /// <param name="cancel">The token to monitor for cancellation requests. The default value is <see cref="P:System.Threading.CancellationToken.None" />.</param> + public static async Task CopyToAsync(Stream source, Stream destination, long? count, int bufferSize, CancellationToken cancel) + { + long? bytesRemaining = count; + + var buffer = ArrayPool<byte>.Shared.Rent(bufferSize); + try + { + Debug.Assert(source != null); + Debug.Assert(destination != null); + Debug.Assert(!bytesRemaining.HasValue || bytesRemaining.Value >= 0); + Debug.Assert(buffer != null); + + while (true) + { + // The natural end of the range. + if (bytesRemaining.HasValue && bytesRemaining.Value <= 0) + { + return; + } + + cancel.ThrowIfCancellationRequested(); + + int readLength = buffer.Length; + if (bytesRemaining.HasValue) + { + readLength = (int)Math.Min(bytesRemaining.Value, (long)readLength); + } + int read = await source.ReadAsync(buffer, 0, readLength, cancel); + + if (bytesRemaining.HasValue) + { + bytesRemaining -= read; + } + + // End of the source stream. + if (read == 0) + { + return; + } + + cancel.ThrowIfCancellationRequested(); + + await destination.WriteAsync(buffer, 0, read, cancel); + } + } + finally + { + ArrayPool<byte>.Shared.Return(buffer); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/UriHelper.cs b/src/Http/Http.Extensions/src/UriHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..633e591186e73c30cbe04055c9581d566ed08329 --- /dev/null +++ b/src/Http/Http.Extensions/src/UriHelper.cs @@ -0,0 +1,217 @@ +// 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.Text; + +namespace Microsoft.AspNetCore.Http.Extensions +{ + /// <summary> + /// A helper class for constructing encoded Uris for use in headers and other Uris. + /// </summary> + public static class UriHelper + { + private const string ForwardSlash = "/"; + private const string Pound = "#"; + private const string QuestionMark = "?"; + private const string SchemeDelimiter = "://"; + + /// <summary> + /// Combines the given URI components into a string that is properly encoded for use in HTTP headers. + /// </summary> + /// <param name="pathBase">The first portion of the request path associated with application root.</param> + /// <param name="path">The portion of the request path that identifies the requested resource.</param> + /// <param name="query">The query, if any.</param> + /// <param name="fragment">The fragment, if any.</param> + /// <returns></returns> + public static string BuildRelative( + PathString pathBase = new PathString(), + PathString path = new PathString(), + QueryString query = new QueryString(), + FragmentString fragment = new FragmentString()) + { + string combinePath = (pathBase.HasValue || path.HasValue) ? (pathBase + path).ToString() : "/"; + return combinePath + query.ToString() + fragment.ToString(); + } + + /// <summary> + /// Combines the given URI components into a string that is properly encoded for use in HTTP headers. + /// Note that unicode in the HostString will be encoded as punycode. + /// </summary> + /// <param name="scheme">http, https, etc.</param> + /// <param name="host">The host portion of the uri normally included in the Host header. This may include the port.</param> + /// <param name="pathBase">The first portion of the request path associated with application root.</param> + /// <param name="path">The portion of the request path that identifies the requested resource.</param> + /// <param name="query">The query, if any.</param> + /// <param name="fragment">The fragment, if any.</param> + /// <returns></returns> + public static string BuildAbsolute( + string scheme, + HostString host, + PathString pathBase = new PathString(), + PathString path = new PathString(), + QueryString query = new QueryString(), + FragmentString fragment = new FragmentString()) + { + if (scheme == null) + { + throw new ArgumentNullException(nameof(scheme)); + } + + var combinedPath = (pathBase.HasValue || path.HasValue) ? (pathBase + path).ToString() : "/"; + + var encodedHost = host.ToString(); + var encodedQuery = query.ToString(); + var encodedFragment = fragment.ToString(); + + // PERF: Calculate string length to allocate correct buffer size for StringBuilder. + var length = scheme.Length + SchemeDelimiter.Length + encodedHost.Length + + combinedPath.Length + encodedQuery.Length + encodedFragment.Length; + + return new StringBuilder(length) + .Append(scheme) + .Append(SchemeDelimiter) + .Append(encodedHost) + .Append(combinedPath) + .Append(encodedQuery) + .Append(encodedFragment) + .ToString(); + } + + /// <summary> + /// Separates the given absolute URI string into components. Assumes no PathBase. + /// </summary> + /// <param name="uri">A string representation of the uri.</param> + /// <param name="scheme">http, https, etc.</param> + /// <param name="host">The host portion of the uri normally included in the Host header. This may include the port.</param> + /// <param name="path">The portion of the request path that identifies the requested resource.</param> + /// <param name="query">The query, if any.</param> + /// <param name="fragment">The fragment, if any.</param> + public static void FromAbsolute( + string uri, + out string scheme, + out HostString host, + out PathString path, + out QueryString query, + out FragmentString fragment) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + path = new PathString(); + query = new QueryString(); + fragment = new FragmentString(); + var startIndex = uri.IndexOf(SchemeDelimiter); + + if (startIndex < 0) + { + throw new FormatException("No scheme delimiter in uri."); + } + + scheme = uri.Substring(0, startIndex); + + // PERF: Calculate the end of the scheme for next IndexOf + startIndex += SchemeDelimiter.Length; + + var searchIndex = -1; + var limit = uri.Length; + + if ((searchIndex = uri.IndexOf(Pound, startIndex)) >= 0 && searchIndex < limit) + { + fragment = FragmentString.FromUriComponent(uri.Substring(searchIndex)); + limit = searchIndex; + } + + if ((searchIndex = uri.IndexOf(QuestionMark, startIndex)) >= 0 && searchIndex < limit) + { + query = QueryString.FromUriComponent(uri.Substring(searchIndex, limit - searchIndex)); + limit = searchIndex; + } + + if ((searchIndex = uri.IndexOf(ForwardSlash, startIndex)) >= 0 && searchIndex < limit) + { + path = PathString.FromUriComponent(uri.Substring(searchIndex, limit - searchIndex)); + limit = searchIndex; + } + + host = HostString.FromUriComponent(uri.Substring(startIndex, limit - startIndex)); + } + + /// <summary> + /// Generates a string from the given absolute or relative Uri that is appropriately encoded for use in + /// HTTP headers. Note that a unicode host name will be encoded as punycode. + /// </summary> + /// <param name="uri">The Uri to encode.</param> + /// <returns></returns> + public static string Encode(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (uri.IsAbsoluteUri) + { + return BuildAbsolute( + scheme: uri.Scheme, + host: HostString.FromUriComponent(uri), + pathBase: PathString.FromUriComponent(uri), + query: QueryString.FromUriComponent(uri), + fragment: FragmentString.FromUriComponent(uri)); + } + else + { + return uri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); + } + } + + /// <summary> + /// Returns the combined components of the request URL in a fully escaped form suitable for use in HTTP headers + /// and other HTTP operations. + /// </summary> + /// <param name="request">The request to assemble the uri pieces from.</param> + /// <returns></returns> + public static string GetEncodedUrl(this HttpRequest request) + { + return BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path, request.QueryString); + } + /// <summary> + /// Returns the relative url + /// </summary> + /// <param name="request">The request to assemble the uri pieces from.</param> + /// <returns></returns> + public static string GetEncodedPathAndQuery(this HttpRequest request) + { + return BuildRelative(request.PathBase, request.Path, request.QueryString); + } + + /// <summary> + /// Returns the combined components of the request URL in a fully un-escaped form (except for the QueryString) + /// suitable only for display. This format should not be used in HTTP headers or other HTTP operations. + /// </summary> + /// <param name="request">The request to assemble the uri pieces from.</param> + /// <returns></returns> + public static string GetDisplayUrl(this HttpRequest request) + { + var host = request.Host.Value; + var pathBase = request.PathBase.Value; + var path = request.Path.Value; + var queryString = request.QueryString.Value; + + // PERF: Calculate string length to allocate correct buffer size for StringBuilder. + var length = request.Scheme.Length + SchemeDelimiter.Length + host.Length + + pathBase.Length + path.Length + queryString.Length; + + return new StringBuilder(length) + .Append(request.Scheme) + .Append(SchemeDelimiter) + .Append(host) + .Append(pathBase) + .Append(path) + .Append(queryString) + .ToString(); + } + } +} diff --git a/src/Http/Http.Extensions/src/baseline.netcore.json b/src/Http/Http.Extensions/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..286133ea54d26159149cb9d7ebdb30252be1186d --- /dev/null +++ b/src/Http/Http.Extensions/src/baseline.netcore.json @@ -0,0 +1,1699 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Http.Extensions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetTypedHeaders", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.Headers.RequestHeaders", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTypedHeaders", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.Headers.ResponseHeaders", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendList<T0>", + "Parameters": [ + { + "Name": "Headers", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.Collections.Generic.IList<T0>" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.ResponseExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Clear", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.SendFileResponseExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "SendFileAsync", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + }, + { + "Name": "file", + "Type": "Microsoft.Extensions.FileProviders.IFileInfo" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SendFileAsync", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + }, + { + "Name": "file", + "Type": "Microsoft.Extensions.FileProviders.IFileInfo" + }, + { + "Name": "offset", + "Type": "System.Int64" + }, + { + "Name": "count", + "Type": "System.Nullable<System.Int64>" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SendFileAsync", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + }, + { + "Name": "fileName", + "Type": "System.String" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SendFileAsync", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + }, + { + "Name": "fileName", + "Type": "System.String" + }, + { + "Name": "offset", + "Type": "System.Int64" + }, + { + "Name": "count", + "Type": "System.Nullable<System.Int64>" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.SessionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "SetInt32", + "Parameters": [ + { + "Name": "session", + "Type": "Microsoft.AspNetCore.Http.ISession" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetInt32", + "Parameters": [ + { + "Name": "session", + "Type": "Microsoft.AspNetCore.Http.ISession" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Nullable<System.Int32>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetString", + "Parameters": [ + { + "Name": "session", + "Type": "Microsoft.AspNetCore.Http.ISession" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetString", + "Parameters": [ + { + "Name": "session", + "Type": "Microsoft.AspNetCore.Http.ISession" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Get", + "Parameters": [ + { + "Name": "session", + "Type": "Microsoft.AspNetCore.Http.ISession" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Byte[]", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Headers.RequestHeaders", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Accept", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.MediaTypeHeaderValue>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Accept", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.MediaTypeHeaderValue>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AcceptCharset", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.StringWithQualityHeaderValue>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AcceptCharset", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.StringWithQualityHeaderValue>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AcceptEncoding", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.StringWithQualityHeaderValue>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AcceptEncoding", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.StringWithQualityHeaderValue>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AcceptLanguage", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.StringWithQualityHeaderValue>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AcceptLanguage", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.StringWithQualityHeaderValue>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CacheControl", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.CacheControlHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CacheControl", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.CacheControlHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentDisposition", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentDisposition", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentLength", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.Int64>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentRange", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.ContentRangeHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentRange", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.ContentRangeHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentType", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentType", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Cookie", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.CookieHeaderValue>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Cookie", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.CookieHeaderValue>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Date", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Date", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Expires", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Expires", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Host", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HostString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Host", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.HostString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IfMatch", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.EntityTagHeaderValue>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IfMatch", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.EntityTagHeaderValue>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IfModifiedSince", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IfModifiedSince", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IfNoneMatch", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.EntityTagHeaderValue>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IfNoneMatch", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.EntityTagHeaderValue>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IfRange", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.RangeConditionHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IfRange", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.RangeConditionHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IfUnmodifiedSince", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IfUnmodifiedSince", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LastModified", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LastModified", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Range", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.RangeHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Range", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.RangeHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Referer", + "Parameters": [], + "ReturnType": "System.Uri", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Referer", + "Parameters": [ + { + "Name": "value", + "Type": "System.Uri" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Get<T0>", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "GetList<T0>", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "System.Collections.Generic.IList<T0>", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Set", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetList<T0>", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.Collections.Generic.IList<T0>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Append", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendList<T0>", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.Collections.Generic.IList<T0>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "headers", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Headers.ResponseHeaders", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CacheControl", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.CacheControlHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CacheControl", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.CacheControlHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentDisposition", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentDisposition", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentLength", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.Int64>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentRange", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.ContentRangeHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentRange", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.ContentRangeHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentType", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentType", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Date", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Date", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ETag", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.EntityTagHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ETag", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.EntityTagHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Expires", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Expires", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LastModified", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LastModified", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Location", + "Parameters": [], + "ReturnType": "System.Uri", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Location", + "Parameters": [ + { + "Name": "value", + "Type": "System.Uri" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SetCookie", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.SetCookieHeaderValue>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SetCookie", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList<Microsoft.Net.Http.Headers.SetCookieHeaderValue>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Get<T0>", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "GetList<T0>", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "System.Collections.Generic.IList<T0>", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Set", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetList<T0>", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.Collections.Generic.IList<T0>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Append", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendList<T0>", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.Collections.Generic.IList<T0>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "headers", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Extensions.HttpRequestMultipartExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetMultipartBoundary", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "ReturnType": "System.String", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Extensions.QueryBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.String, System.String>>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.Collections.Generic.IEnumerable<System.String>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToQueryString", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<System.String, System.String>>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.String, System.String>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "parameters", + "Type": "System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.String, System.String>>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CopyToAsync", + "Parameters": [ + { + "Name": "source", + "Type": "System.IO.Stream" + }, + { + "Name": "destination", + "Type": "System.IO.Stream" + }, + { + "Name": "count", + "Type": "System.Nullable<System.Int64>" + }, + { + "Name": "cancel", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CopyToAsync", + "Parameters": [ + { + "Name": "source", + "Type": "System.IO.Stream" + }, + { + "Name": "destination", + "Type": "System.IO.Stream" + }, + { + "Name": "count", + "Type": "System.Nullable<System.Int64>" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + }, + { + "Name": "cancel", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Extensions.UriHelper", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "BuildRelative", + "Parameters": [ + { + "Name": "pathBase", + "Type": "Microsoft.AspNetCore.Http.PathString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.PathString)" + }, + { + "Name": "path", + "Type": "Microsoft.AspNetCore.Http.PathString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.PathString)" + }, + { + "Name": "query", + "Type": "Microsoft.AspNetCore.Http.QueryString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.QueryString)" + }, + { + "Name": "fragment", + "Type": "Microsoft.AspNetCore.Http.FragmentString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.FragmentString)" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "BuildAbsolute", + "Parameters": [ + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "host", + "Type": "Microsoft.AspNetCore.Http.HostString" + }, + { + "Name": "pathBase", + "Type": "Microsoft.AspNetCore.Http.PathString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.PathString)" + }, + { + "Name": "path", + "Type": "Microsoft.AspNetCore.Http.PathString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.PathString)" + }, + { + "Name": "query", + "Type": "Microsoft.AspNetCore.Http.QueryString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.QueryString)" + }, + { + "Name": "fragment", + "Type": "Microsoft.AspNetCore.Http.FragmentString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.FragmentString)" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromAbsolute", + "Parameters": [ + { + "Name": "uri", + "Type": "System.String" + }, + { + "Name": "scheme", + "Type": "System.String", + "Direction": "Out" + }, + { + "Name": "host", + "Type": "Microsoft.AspNetCore.Http.HostString", + "Direction": "Out" + }, + { + "Name": "path", + "Type": "Microsoft.AspNetCore.Http.PathString", + "Direction": "Out" + }, + { + "Name": "query", + "Type": "Microsoft.AspNetCore.Http.QueryString", + "Direction": "Out" + }, + { + "Name": "fragment", + "Type": "Microsoft.AspNetCore.Http.FragmentString", + "Direction": "Out" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Encode", + "Parameters": [ + { + "Name": "uri", + "Type": "System.Uri" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetEncodedUrl", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "ReturnType": "System.String", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetEncodedPathAndQuery", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "ReturnType": "System.String", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDisplayUrl", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "ReturnType": "System.String", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/HeaderDictionaryTypeExtensionsTest.cs b/src/Http/Http.Extensions/test/HeaderDictionaryTypeExtensionsTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..1d01466284a7d9b2aeecb370e247273f3e6f81e4 --- /dev/null +++ b/src/Http/Http.Extensions/test/HeaderDictionaryTypeExtensionsTest.cs @@ -0,0 +1,205 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Headers +{ + public class HeaderDictionaryTypeExtensionsTest + { + [Fact] + public void GetT_KnownTypeWithValidValue_Success() + { + var context = new DefaultHttpContext(); + context.Request.Headers[HeaderNames.ContentType] = "text/plain"; + + var result = context.Request.GetTypedHeaders().Get<MediaTypeHeaderValue>(HeaderNames.ContentType); + + var expected = new MediaTypeHeaderValue("text/plain"); + Assert.Equal(expected, result); + } + + [Fact] + public void GetT_KnownTypeWithMissingValue_Null() + { + var context = new DefaultHttpContext(); + + var result = context.Request.GetTypedHeaders().Get<MediaTypeHeaderValue>(HeaderNames.ContentType); + + Assert.Null(result); + } + + [Fact] + public void GetT_KnownTypeWithInvalidValue_Null() + { + var context = new DefaultHttpContext(); + context.Request.Headers[HeaderNames.ContentType] = "invalid"; + + var result = context.Request.GetTypedHeaders().Get<MediaTypeHeaderValue>(HeaderNames.ContentType); + + Assert.Null(result); + } + + [Fact] + public void GetT_UnknownTypeWithTryParseAndValidValue_Success() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "valid"; + + var result = context.Request.GetTypedHeaders().Get<TestHeaderValue>("custom"); + Assert.NotNull(result); + } + + [Fact] + public void GetT_UnknownTypeWithTryParseAndInvalidValue_Null() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "invalid"; + + var result = context.Request.GetTypedHeaders().Get<TestHeaderValue>("custom"); + Assert.Null(result); + } + + [Fact] + public void GetT_UnknownTypeWithTryParseAndMissingValue_Null() + { + var context = new DefaultHttpContext(); + + var result = context.Request.GetTypedHeaders().Get<TestHeaderValue>("custom"); + Assert.Null(result); + } + + [Fact] + public void GetT_UnknownTypeWithoutTryParse_Throws() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "valid"; + + Assert.Throws<NotSupportedException>(() => context.Request.GetTypedHeaders().Get<object>("custom")); + } + + [Fact] + public void GetListT_KnownTypeWithValidValue_Success() + { + var context = new DefaultHttpContext(); + context.Request.Headers[HeaderNames.Accept] = "text/plain; q=0.9, text/other, */*"; + + var result = context.Request.GetTypedHeaders().GetList<MediaTypeHeaderValue>(HeaderNames.Accept); + + var expected = new[] { + new MediaTypeHeaderValue("text/plain", 0.9), + new MediaTypeHeaderValue("text/other"), + new MediaTypeHeaderValue("*/*"), + }.ToList(); + Assert.Equal(expected, result); + } + + [Fact] + public void GetListT_KnownTypeWithMissingValue_Null() + { + var context = new DefaultHttpContext(); + + var result = context.Request.GetTypedHeaders().GetList<MediaTypeHeaderValue>(HeaderNames.Accept); + + Assert.Null(result); + } + + [Fact] + public void GetListT_KnownTypeWithInvalidValue_Null() + { + var context = new DefaultHttpContext(); + context.Request.Headers[HeaderNames.Accept] = "invalid"; + + var result = context.Request.GetTypedHeaders().GetList<MediaTypeHeaderValue>(HeaderNames.Accept); + + Assert.Null(result); + } + + [Fact] + public void GetListT_UnknownTypeWithTryParseListAndValidValue_Success() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "valid"; + + var results = context.Request.GetTypedHeaders().GetList<TestHeaderValue>("custom"); + Assert.NotNull(results); + Assert.Equal(new[] { new TestHeaderValue() }.ToList(), results); + } + + [Fact] + public void GetListT_UnknownTypeWithTryParseListAndInvalidValue_Null() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "invalid"; + + var results = context.Request.GetTypedHeaders().GetList<TestHeaderValue>("custom"); + Assert.Null(results); + } + + [Fact] + public void GetListT_UnknownTypeWithTryParseListAndMissingValue_Null() + { + var context = new DefaultHttpContext(); + + var results = context.Request.GetTypedHeaders().GetList<TestHeaderValue>("custom"); + Assert.Null(results); + } + + [Fact] + public void GetListT_UnknownTypeWithoutTryParseList_Throws() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "valid"; + + Assert.Throws<NotSupportedException>(() => context.Request.GetTypedHeaders().GetList<object>("custom")); + } + + public class TestHeaderValue + { + public static bool TryParse(string value, out TestHeaderValue result) + { + if (string.Equals("valid", value)) + { + result = new TestHeaderValue(); + return true; + } + result = null; + return false; + } + + public static bool TryParseList(IList<string> values, out IList<TestHeaderValue> result) + { + var results = new List<TestHeaderValue>(); + foreach (var value in values) + { + if (string.Equals("valid", value)) + { + results.Add(new TestHeaderValue()); + } + } + if (results.Count > 0) + { + result = results; + return true; + } + result = null; + return false; + } + + public override bool Equals(object obj) + { + var other = obj as TestHeaderValue; + return other != null; + } + + public override int GetHashCode() + { + return 0; + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..fae14d9f7aacab41df367362573bffd65314d6dd --- /dev/null +++ b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj @@ -0,0 +1,13 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Http" /> + <Reference Include="Microsoft.AspNetCore.Http.Extensions" /> + <Reference Include="Microsoft.Extensions.DependencyInjection" /> + </ItemGroup> + +</Project> diff --git a/src/Http/Http.Extensions/test/QueryBuilderTests.cs b/src/Http/Http.Extensions/test/QueryBuilderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..7d15dd87bfe01520ca7b3e35fb17551005dd54f7 --- /dev/null +++ b/src/Http/Http.Extensions/test/QueryBuilderTests.cs @@ -0,0 +1,98 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Extensions +{ + public class QueryBuilderTests + { + [Fact] + public void EmptyQuery_NoQuestionMark() + { + var builder = new QueryBuilder(); + Assert.Equal(string.Empty, builder.ToString()); + } + + [Fact] + public void AddSimple_NoEncoding() + { + var builder = new QueryBuilder(); + builder.Add("key", "value"); + Assert.Equal("?key=value", builder.ToString()); + } + + [Fact] + public void AddSpace_PercentEncoded() + { + var builder = new QueryBuilder(); + builder.Add("key", "value 1"); + Assert.Equal("?key=value%201", builder.ToString()); + } + + [Fact] + public void AddReservedCharacters_PercentEncoded() + { + var builder = new QueryBuilder(); + builder.Add("key&", "value#"); + Assert.Equal("?key%26=value%23", builder.ToString()); + } + + [Fact] + public void AddMultipleValues_AddedInOrder() + { + var builder = new QueryBuilder(); + builder.Add("key1", "value1"); + builder.Add("key2", "value2"); + builder.Add("key3", "value3"); + Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString()); + } + + [Fact] + public void AddIEnumerableValues_AddedInOrder() + { + var builder = new QueryBuilder(); + builder.Add("key", new[] { "value1", "value2", "value3" }); + Assert.Equal("?key=value1&key=value2&key=value3", builder.ToString()); + } + + [Fact] + public void AddMultipleValuesViaConstructor_AddedInOrder() + { + var builder = new QueryBuilder(new[] + { + new KeyValuePair<string, string>("key1", "value1"), + new KeyValuePair<string, string>("key2", "value2"), + new KeyValuePair<string, string>("key3", "value3"), + }); + Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString()); + } + + [Fact] + public void AddMultipleValuesViaInitializer_AddedInOrder() + { + var builder = new QueryBuilder() + { + { "key1", "value1" }, + { "key2", "value2" }, + { "key3", "value3" }, + }; + Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString()); + } + + [Fact] + public void CopyViaConstructor_AddedInOrder() + { + var builder = new QueryBuilder() + { + { "key1", "value1" }, + { "key2", "value2" }, + { "key3", "value3" }, + }; + var builder1 = new QueryBuilder(builder); + Assert.Equal("?key1=value1&key2=value2&key3=value3", builder1.ToString()); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/ResponseExtensionTests.cs b/src/Http/Http.Extensions/test/ResponseExtensionTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..ae6b147fd2231b56cab941a351eb16f9d9e3d28c --- /dev/null +++ b/src/Http/Http.Extensions/test/ResponseExtensionTests.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.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Extensions +{ + public class ResponseExtensionTests + { + [Fact] + public void Clear_ResetsResponse() + { + var context = new DefaultHttpContext(); + context.Response.StatusCode = 201; + context.Response.Headers["custom"] = "value"; + context.Response.Body.Write(new byte[100], 0, 100); + + context.Response.Clear(); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Response.Headers["custom"].ToString()); + Assert.Equal(0, context.Response.Body.Length); + } + + [Fact] + public void Clear_AlreadyStarted_Throws() + { + var context = new DefaultHttpContext(); + context.Features.Set<IHttpResponseFeature>(new StartedResponseFeature()); + + Assert.Throws<InvalidOperationException>(() => context.Response.Clear()); + } + + private class StartedResponseFeature : IHttpResponseFeature + { + public Stream Body { get; set; } + + public bool HasStarted { get { return true; } } + + public IHeaderDictionary Headers { get; set; } + + public string ReasonPhrase { get; set; } + + public int StatusCode { get; set; } + + public void OnCompleted(Func<object, Task> callback, object state) + { + throw new NotImplementedException(); + } + + public void OnStarting(Func<object, Task> callback, object state) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Http/Http.Extensions/test/SendFileResponseExtensionsTests.cs b/src/Http/Http.Extensions/test/SendFileResponseExtensionsTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..f4c7c0f2a9918a84d1a1283a75d57bc7c6b14fac --- /dev/null +++ b/src/Http/Http.Extensions/test/SendFileResponseExtensionsTests.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Extensions.Tests +{ + public class SendFileResponseExtensionsTests + { + [Fact] + public Task SendFileWhenFileNotFoundThrows() + { + var response = new DefaultHttpContext().Response; + return Assert.ThrowsAsync<FileNotFoundException>(() => response.SendFileAsync("foo")); + } + + [Fact] + public async Task SendFileWorks() + { + var context = new DefaultHttpContext(); + var response = context.Response; + var fakeFeature = new FakeSendFileFeature(); + context.Features.Set<IHttpSendFileFeature>(fakeFeature); + + await response.SendFileAsync("bob", 1, 3, CancellationToken.None); + + Assert.Equal("bob", fakeFeature.name); + Assert.Equal(1, fakeFeature.offset); + Assert.Equal(3, fakeFeature.length); + Assert.Equal(CancellationToken.None, fakeFeature.token); + } + + private class FakeSendFileFeature : IHttpSendFileFeature + { + public string name = null; + public long offset = 0; + public long? length = null; + public CancellationToken token; + + public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) + { + this.name = path; + this.offset = offset; + this.length = length; + this.token = cancellation; + return Task.FromResult(0); + } + } + } +} diff --git a/src/Http/Http.Extensions/test/UriHelperTests.cs b/src/Http/Http.Extensions/test/UriHelperTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..11b045af4fdeb885c53b629bd2037fb0fdeefea4 --- /dev/null +++ b/src/Http/Http.Extensions/test/UriHelperTests.cs @@ -0,0 +1,156 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Http.Extensions +{ + public class UriHelperTests + { + [Fact] + public void EncodeEmptyPartialUrl() + { + var result = UriHelper.BuildRelative(); + + Assert.Equal("/", result); + } + + [Fact] + public void EncodePartialUrl() + { + var result = UriHelper.BuildRelative(new PathString("/un?escaped/base"), new PathString("/un?escaped"), + new QueryString("?name=val%23ue"), new FragmentString("#my%20value")); + + Assert.Equal("/un%3Fescaped/base/un%3Fescaped?name=val%23ue#my%20value", result); + } + + [Fact] + public void EncodeEmptyFullUrl() + { + var result = UriHelper.BuildAbsolute("http", new HostString(string.Empty)); + + Assert.Equal("http:///", result); + } + + [Fact] + public void EncodeFullUrl() + { + var result = UriHelper.BuildAbsolute("http", new HostString("my.HoΨst:80"), new PathString("/un?escaped/base"), new PathString("/un?escaped"), + new QueryString("?name=val%23ue"), new FragmentString("#my%20value")); + + Assert.Equal("http://my.xn--host-cpd:80/un%3Fescaped/base/un%3Fescaped?name=val%23ue#my%20value", result); + } + + [Fact] + public void GetEncodedUrlFromRequest() + { + var request = new DefaultHttpContext().Request; + request.Scheme = "http"; + request.Host = new HostString("my.HoΨst:80"); + request.PathBase = new PathString("/un?escaped/base"); + request.Path = new PathString("/un?escaped"); + request.QueryString = new QueryString("?name=val%23ue"); + + Assert.Equal("http://my.xn--host-cpd:80/un%3Fescaped/base/un%3Fescaped?name=val%23ue", request.GetEncodedUrl()); + } + + [Fact] + public void GetDisplayUrlFromRequest() + { + var request = new DefaultHttpContext().Request; + request.Scheme = "http"; + request.Host = new HostString("my.HoΨst:80"); + request.PathBase = new PathString("/un?escaped/base"); + request.Path = new PathString("/un?escaped"); + request.QueryString = new QueryString("?name=val%23ue"); + + Assert.Equal("http://my.hoψst:80/un?escaped/base/un?escaped?name=val%23ue", request.GetDisplayUrl()); + } + + [Theory] + [InlineData("http://example.com", "http", "example.com", "", "", "")] + [InlineData("https://example.com", "https", "example.com", "", "", "")] + [InlineData("http://example.com/foo/bar", "http", "example.com", "/foo/bar", "", "")] + [InlineData("http://example.com/foo/bar?baz=1", "http", "example.com", "/foo/bar", "?baz=1", "")] + [InlineData("http://example.com/foo#col=2", "http", "example.com", "/foo", "", "#col=2")] + [InlineData("http://example.com/foo?bar=1#col=2", "http", "example.com", "/foo", "?bar=1", "#col=2")] + [InlineData("http://example.com?bar=1#col=2", "http", "example.com", "", "?bar=1", "#col=2")] + [InlineData("http://example.com#frag?stillfrag/stillfrag", "http", "example.com", "", "", "#frag?stillfrag/stillfrag")] + [InlineData("http://example.com?q/stillq#frag?stillfrag/stillfrag", "http", "example.com", "", "?q/stillq", "#frag?stillfrag/stillfrag")] + [InlineData("http://example.com/fo%23o#col=2", "http", "example.com", "/fo#o", "", "#col=2")] + [InlineData("http://example.com/fo%3Fo#col=2", "http", "example.com", "/fo?o", "", "#col=2")] + [InlineData("ftp://example.com/", "ftp", "example.com", "/", "", "")] + [InlineData("https://127.0.0.0:80/bar", "https", "127.0.0.0:80", "/bar", "", "")] + [InlineData("http://[1080:0:0:0:8:800:200C:417A]/index.html", "http", "[1080:0:0:0:8:800:200C:417A]", "/index.html", "", "")] + [InlineData("http://example.com///", "http", "example.com", "///", "", "")] + public void FromAbsoluteUriParsingChecks( + string uri, + string expectedScheme, + string expectedHost, + string expectedPath, + string expectedQuery, + string expectedFragment) + { + string scheme = null; + var host = new HostString(); + var path = new PathString(); + var query = new QueryString(); + var fragment = new FragmentString(); + UriHelper.FromAbsolute(uri, out scheme, out host, out path, out query, out fragment); + + Assert.Equal(scheme, expectedScheme); + Assert.Equal(host, new HostString(expectedHost)); + Assert.Equal(path, new PathString(expectedPath)); + Assert.Equal(query, new QueryString(expectedQuery)); + Assert.Equal(fragment, new FragmentString(expectedFragment)); + } + + [Fact] + public void FromAbsoluteToBuildAbsolute() + { + var scheme = "http"; + var host = new HostString("example.com"); + var path = new PathString("/index.html"); + var query = new QueryString("?foo=1"); + var fragment = new FragmentString("#col=1"); + var request = UriHelper.BuildAbsolute(scheme, host, path:path, query:query, fragment:fragment); + + string resScheme = null; + var resHost = new HostString(); + var resPath = new PathString(); + var resQuery = new QueryString(); + var resFragment = new FragmentString(); + UriHelper.FromAbsolute(request, out resScheme, out resHost, out resPath, out resQuery, out resFragment); + + Assert.Equal(scheme, resScheme); + Assert.Equal(host, resHost); + Assert.Equal(path, resPath); + Assert.Equal(query, resQuery); + Assert.Equal(fragment, resFragment); + } + + [Fact] + public void BuildAbsoluteNullInputThrowsArgumentNullException() + { + var resHost = new HostString(); + var resPath = new PathString(); + var resQuery = new QueryString(); + var resFragment = new FragmentString(); + Assert.Throws<ArgumentNullException>(() => UriHelper.BuildAbsolute(null, resHost, resPath, resPath, resQuery, resFragment)); + + } + + [Fact] + public void FromAbsoluteNullInputThrowsArgumentNullException() + { + string resScheme = null; + var resHost = new HostString(); + var resPath = new PathString(); + var resQuery = new QueryString(); + var resFragment = new FragmentString(); + Assert.Throws<ArgumentNullException>(() => UriHelper.FromAbsolute(null, out resScheme, out resHost, out resPath, out resQuery, out resFragment)); + + } + } +} diff --git a/src/Http/Http.Features/src/Authentication/AuthenticateContext.cs b/src/Http/Http.Features/src/Authentication/AuthenticateContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..e73061667b4a6abd091fa3eac4c86d3305e0c9a4 --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/AuthenticateContext.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public class AuthenticateContext + { + public AuthenticateContext(string authenticationScheme) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + + AuthenticationScheme = authenticationScheme; + } + + public string AuthenticationScheme { get; } + + public bool Accepted { get; private set; } + + public ClaimsPrincipal Principal { get; private set; } + + public IDictionary<string, string> Properties { get; private set; } + + public IDictionary<string, object> Description { get; private set; } + + public Exception Error { get; private set; } + + public virtual void Authenticated(ClaimsPrincipal principal, IDictionary<string, string> properties, IDictionary<string, object> description) + { + Accepted = true; + + Principal = principal; + Properties = properties; + Description = description; + + // Set defaults for fields we don't use in case multiple handlers modified the context. + Error = null; + } + + public virtual void NotAuthenticated() + { + Accepted = true; + + // Set defaults for fields we don't use in case multiple handlers modified the context. + Description = null; + Error = null; + Principal = null; + Properties = null; + } + + public virtual void Failed(Exception error) + { + Accepted = true; + + Error = error; + + // Set defaults for fields we don't use in case multiple handlers modified the context. + Description = null; + Principal = null; + Properties = null; + } + } +} diff --git a/src/Http/Http.Features/src/Authentication/ChallengeBehavior.cs b/src/Http/Http.Features/src/Authentication/ChallengeBehavior.cs new file mode 100644 index 0000000000000000000000000000000000000000..549d51132a70e2c667c8311da68da87f80b85c17 --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/ChallengeBehavior.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public enum ChallengeBehavior + { + Automatic, + Unauthorized, + Forbidden + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/Authentication/ChallengeContext.cs b/src/Http/Http.Features/src/Authentication/ChallengeContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..c0fe470806aadc076b5672fb96cbeeca6d665424 --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/ChallengeContext.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public class ChallengeContext + { + public ChallengeContext(string authenticationScheme) + : this(authenticationScheme, properties: null, behavior: ChallengeBehavior.Automatic) + { + } + + public ChallengeContext(string authenticationScheme, IDictionary<string, string> properties, ChallengeBehavior behavior) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + + AuthenticationScheme = authenticationScheme; + Properties = properties ?? new Dictionary<string, string>(StringComparer.Ordinal); + Behavior = behavior; + } + + public string AuthenticationScheme { get; } + + public ChallengeBehavior Behavior { get; } + + public IDictionary<string, string> Properties { get; } + + public bool Accepted { get; private set; } + + public void Accept() + { + Accepted = true; + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/Authentication/DescribeSchemesContext.cs b/src/Http/Http.Features/src/Authentication/DescribeSchemesContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..b25c2c979ac0999d6117c22941a93d06737bb1d9 --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/DescribeSchemesContext.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public class DescribeSchemesContext + { + private List<IDictionary<string, object>> _results; + + public DescribeSchemesContext() + { + _results = new List<IDictionary<string, object>>(); + } + + public IEnumerable<IDictionary<string, object>> Results + { + get { return _results; } + } + + public void Accept(IDictionary<string, object> description) + { + _results.Add(description); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/Authentication/IAuthenticationHandler.cs b/src/Http/Http.Features/src/Authentication/IAuthenticationHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..3b7236418290f7257a9afe56aea87abc0a65f24f --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/IAuthenticationHandler.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public interface IAuthenticationHandler + { + void GetDescriptions(DescribeSchemesContext context); + + Task AuthenticateAsync(AuthenticateContext context); + + Task ChallengeAsync(ChallengeContext context); + + Task SignInAsync(SignInContext context); + + Task SignOutAsync(SignOutContext context); + } +} diff --git a/src/Http/Http.Features/src/Authentication/IHttpAuthenticationFeature.cs b/src/Http/Http.Features/src/Authentication/IHttpAuthenticationFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..279d6904f08ac9d5ef1ff553bb0053eafa73c823 --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/IHttpAuthenticationFeature.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public interface IHttpAuthenticationFeature + { + ClaimsPrincipal User { get; set; } + + [Obsolete("This is obsolete and will be removed in a future version. See https://go.microsoft.com/fwlink/?linkid=845470.")] + IAuthenticationHandler Handler { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/Authentication/SignInContext.cs b/src/Http/Http.Features/src/Authentication/SignInContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..f04dade51b994d77faf311ce4c88dde2030e8e82 --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/SignInContext.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public class SignInContext + { + public SignInContext(string authenticationScheme, ClaimsPrincipal principal, IDictionary<string, string> properties) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + AuthenticationScheme = authenticationScheme; + Principal = principal; + Properties = properties ?? new Dictionary<string, string>(StringComparer.Ordinal); + } + + public string AuthenticationScheme { get; } + + public ClaimsPrincipal Principal { get; } + + public IDictionary<string, string> Properties { get; } + + public bool Accepted { get; private set; } + + public void Accept() + { + Accepted = true; + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/Authentication/SignOutContext.cs b/src/Http/Http.Features/src/Authentication/SignOutContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..c752f057dfe01cab2222e16b0c55166cfaacd226 --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/SignOutContext.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public class SignOutContext + { + public SignOutContext(string authenticationScheme, IDictionary<string, string> properties) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + + AuthenticationScheme = authenticationScheme; + Properties = properties ?? new Dictionary<string, string>(StringComparer.Ordinal); + } + + public string AuthenticationScheme { get; } + + public IDictionary<string, string> Properties { get; } + + public bool Accepted { get; private set; } + + public void Accept() + { + Accepted = true; + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/CookieOptions.cs b/src/Http/Http.Features/src/CookieOptions.cs new file mode 100644 index 0000000000000000000000000000000000000000..27141a32f2864a5e35c27ecd90a630796e4c594b --- /dev/null +++ b/src/Http/Http.Features/src/CookieOptions.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Options used to create a new cookie. + /// </summary> + public class CookieOptions + { + /// <summary> + /// Creates a default cookie with a path of '/'. + /// </summary> + public CookieOptions() + { + Path = "/"; + } + + /// <summary> + /// Gets or sets the domain to associate the cookie with. + /// </summary> + /// <returns>The domain to associate the cookie with.</returns> + public string Domain { get; set; } + + /// <summary> + /// Gets or sets the cookie path. + /// </summary> + /// <returns>The cookie path.</returns> + public string Path { get; set; } + + /// <summary> + /// Gets or sets the expiration date and time for the cookie. + /// </summary> + /// <returns>The expiration date and time for the cookie.</returns> + public DateTimeOffset? Expires { get; set; } + + /// <summary> + /// Gets or sets a value that indicates whether to transmit the cookie using Secure Sockets Layer (SSL)--that is, over HTTPS only. + /// </summary> + /// <returns>true to transmit the cookie only over an SSL connection (HTTPS); otherwise, false.</returns> + public bool Secure { get; set; } + + /// <summary> + /// Gets or sets the value for the SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.Lax"/> + /// </summary> + /// <returns>The <see cref="SameSiteMode"/> representing the enforcement mode of the cookie.</returns> + public SameSiteMode SameSite { get; set; } = SameSiteMode.Lax; + + /// <summary> + /// Gets or sets a value that indicates whether a cookie is accessible by client-side script. + /// </summary> + /// <returns>true if a cookie must not be accessible by client-side script; otherwise, false.</returns> + public bool HttpOnly { get; set; } + + /// <summary> + /// Gets or sets the max-age for the cookie. + /// </summary> + /// <returns>The max-age date and time for the cookie.</returns> + public TimeSpan? MaxAge { get; set; } + + /// <summary> + /// Indicates if this cookie is essential for the application to function correctly. If true then + /// consent policy checks may be bypassed. The default value is false. + /// </summary> + public bool IsEssential { get; set; } + } +} diff --git a/src/Http/Http.Features/src/FeatureCollection.cs b/src/Http/Http.Features/src/FeatureCollection.cs new file mode 100644 index 0000000000000000000000000000000000000000..e79ecfee224aca99d3186735357888dd9560651b --- /dev/null +++ b/src/Http/Http.Features/src/FeatureCollection.cs @@ -0,0 +1,119 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class FeatureCollection : IFeatureCollection + { + private static KeyComparer FeatureKeyComparer = new KeyComparer(); + private readonly IFeatureCollection _defaults; + private IDictionary<Type, object> _features; + private volatile int _containerRevision; + + public FeatureCollection() + { + } + + public FeatureCollection(IFeatureCollection defaults) + { + _defaults = defaults; + } + + public virtual int Revision + { + get { return _containerRevision + (_defaults?.Revision ?? 0); } + } + + public bool IsReadOnly { get { return false; } } + + public object this[Type key] + { + get + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + object result; + return _features != null && _features.TryGetValue(key, out result) ? result : _defaults?[key]; + } + set + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (value == null) + { + if (_features != null && _features.Remove(key)) + { + _containerRevision++; + } + return; + } + + if (_features == null) + { + _features = new Dictionary<Type, object>(); + } + _features[key] = value; + _containerRevision++; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IEnumerator<KeyValuePair<Type, object>> GetEnumerator() + { + if (_features != null) + { + foreach (var pair in _features) + { + yield return pair; + } + } + + if (_defaults != null) + { + // Don't return features masked by the wrapper. + foreach (var pair in _features == null ? _defaults : _defaults.Except(_features, FeatureKeyComparer)) + { + yield return pair; + } + } + } + + public TFeature Get<TFeature>() + { + return (TFeature)this[typeof(TFeature)]; + } + + public void Set<TFeature>(TFeature instance) + { + this[typeof(TFeature)] = instance; + } + + private class KeyComparer : IEqualityComparer<KeyValuePair<Type, object>> + { + public bool Equals(KeyValuePair<Type, object> x, KeyValuePair<Type, object> y) + { + return x.Key.Equals(y.Key); + } + + public int GetHashCode(KeyValuePair<Type, object> obj) + { + return obj.Key.GetHashCode(); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/FeatureReference.cs b/src/Http/Http.Features/src/FeatureReference.cs new file mode 100644 index 0000000000000000000000000000000000000000..5016602123106e2fa73e7e8cf2a5af176990ebbd --- /dev/null +++ b/src/Http/Http.Features/src/FeatureReference.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features +{ + public struct FeatureReference<T> + { + private T _feature; + private int _revision; + + private FeatureReference(T feature, int revision) + { + _feature = feature; + _revision = revision; + } + + public static readonly FeatureReference<T> Default = new FeatureReference<T>(default(T), -1); + + public T Fetch(IFeatureCollection features) + { + if (_revision == features.Revision) + { + return _feature; + } + _feature = (T)features[typeof(T)]; + _revision = features.Revision; + return _feature; + } + + public T Update(IFeatureCollection features, T feature) + { + features[typeof(T)] = feature; + _feature = feature; + _revision = features.Revision; + return feature; + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/FeatureReferences.cs b/src/Http/Http.Features/src/FeatureReferences.cs new file mode 100644 index 0000000000000000000000000000000000000000..38bd2ec27aa6f57bc5370935863213b034a6e1b4 --- /dev/null +++ b/src/Http/Http.Features/src/FeatureReferences.cs @@ -0,0 +1,98 @@ +// 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.Runtime.CompilerServices; + +namespace Microsoft.AspNetCore.Http.Features +{ + public struct FeatureReferences<TCache> + { + public FeatureReferences(IFeatureCollection collection) + { + Collection = collection; + Cache = default(TCache); + Revision = collection.Revision; + } + + public IFeatureCollection Collection { get; private set; } + public int Revision { get; private set; } + + // cache is a public field because the code calling Fetch must + // be able to pass ref values that "dot through" the TCache struct memory, + // if it was a Property then that getter would return a copy of the memory + // preventing the use of "ref" + public TCache Cache; + + // Careful with modifications to the Fetch method; it is carefully constructed for inlining + // See: https://github.com/aspnet/HttpAbstractions/pull/704 + // This method is 59 IL bytes and at inline call depth 3 from accessing a property. + // This combination is enough for the jit to consider it an "unprofitable inline" + // Aggressively inlining it causes the entire call chain to dissolve: + // + // This means this call graph: + // + // HttpResponse.Headers -> Response.HttpResponseFeature -> Fetch -> Fetch -> Revision + // -> Collection -> Collection + // -> Collection.Revision + // Has 6 calls eliminated and becomes just: -> UpdateCached + // + // HttpResponse.Headers -> Collection.Revision + // -> UpdateCached (not called on fast path) + // + // As this is inlined at the callsite we want to keep the method small, so it only detects + // if a reset or update is required and all the reset and update logic is pushed to UpdateCached. + // + // Generally Fetch is called at a ratio > x4 of UpdateCached so this is a large gain + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TFeature Fetch<TFeature, TState>( + ref TFeature cached, + TState state, + Func<TState, TFeature> factory) where TFeature : class + { + var flush = false; + var revision = Collection.Revision; + if (Revision != revision) + { + // Clear cached value to force call to UpdateCached + cached = null; + // Collection changed, clear whole feature cache + flush = true; + } + + return cached ?? UpdateCached(ref cached, state, factory, revision, flush); + } + + // Update and cache clearing logic, when the fast-path in Fetch isn't applicable + private TFeature UpdateCached<TFeature, TState>(ref TFeature cached, TState state, Func<TState, TFeature> factory, int revision, bool flush) where TFeature : class + { + if (flush) + { + // Collection detected as changed, clear cache + Cache = default(TCache); + } + + cached = Collection.Get<TFeature>(); + if (cached == null) + { + // Item not in collection, create it with factory + cached = factory(state); + // Add item to IFeatureCollection + Collection.Set(cached); + // Revision changed by .Set, update revision to new value + Revision = Collection.Revision; + } + else if (flush) + { + // Cache was cleared, but item retrived from current Collection for version + // so use passed in revision rather than making another virtual call + Revision = revision; + } + + return cached; + } + + public TFeature Fetch<TFeature>(ref TFeature cached, Func<IFeatureCollection, TFeature> factory) + where TFeature : class => Fetch(ref cached, Collection, factory); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IFeatureCollection.cs b/src/Http/Http.Features/src/IFeatureCollection.cs new file mode 100644 index 0000000000000000000000000000000000000000..f7b23ed16f25d98f7e157ded3672e09bf6272277 --- /dev/null +++ b/src/Http/Http.Features/src/IFeatureCollection.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// <summary> + /// Represents a collection of HTTP features. + /// </summary> + public interface IFeatureCollection : IEnumerable<KeyValuePair<Type, object>> + { + /// <summary> + /// Indicates if the collection can be modified. + /// </summary> + bool IsReadOnly { get; } + + /// <summary> + /// Incremented for each modification and can be used to verify cached results. + /// </summary> + int Revision { get; } + + /// <summary> + /// Gets or sets a given feature. Setting a null value removes the feature. + /// </summary> + /// <param name="key"></param> + /// <returns>The requested feature, or null if it is not present.</returns> + object this[Type key] { get; set; } + + /// <summary> + /// Retrieves the requested feature from the collection. + /// </summary> + /// <typeparam name="TFeature">The feature key.</typeparam> + /// <returns>The requested feature, or null if it is not present.</returns> + TFeature Get<TFeature>(); + + /// <summary> + /// Sets the given feature in the collection. + /// </summary> + /// <typeparam name="TFeature">The feature key.</typeparam> + /// <param name="instance">The feature value.</param> + void Set<TFeature>(TFeature instance); + } +} diff --git a/src/Http/Http.Features/src/IFormCollection.cs b/src/Http/Http.Features/src/IFormCollection.cs new file mode 100644 index 0000000000000000000000000000000000000000..237d311ae801c600a1933573528a86fa9a1c8b63 --- /dev/null +++ b/src/Http/Http.Features/src/IFormCollection.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Represents the parsed form values sent with the HttpRequest. + /// </summary> + public interface IFormCollection : IEnumerable<KeyValuePair<string, StringValues>> + { + /// <summary> + /// Gets the number of elements contained in the <see cref="IFormCollection" />. + /// </summary> + /// <returns> + /// The number of elements contained in the <see cref="IFormCollection" />. + /// </returns> + int Count { get; } + + /// <summary> + /// Gets an <see cref="ICollection{T}" /> containing the keys of the + /// <see cref="IFormCollection" />. + /// </summary> + /// <returns> + /// An <see cref="ICollection{T}" /> containing the keys of the object + /// that implements <see cref="IFormCollection" />. + /// </returns> + ICollection<string> Keys { get; } + + /// <summary> + /// Determines whether the <see cref="IFormCollection" /> contains an element + /// with the specified key. + /// </summary> + /// <param name="key"> + /// The key to locate in the <see cref="IFormCollection" />. + /// </param> + /// <returns> + /// true if the <see cref="IFormCollection" /> contains an element with + /// the key; otherwise, false. + /// </returns> + /// <exception cref="System.ArgumentNullException"> + /// key is null. + /// </exception> + bool ContainsKey(string key); + + /// <summary> + /// Gets the value associated with the specified key. + /// </summary> + /// <param name="key"> + /// The key of the value to get. + /// </param> + /// <param name="value"> + /// The key of the value to get. + /// When this method returns, the value associated with the specified key, if the + /// key is found; otherwise, the default value for the type of the value parameter. + /// This parameter is passed uninitialized. + /// </param> + /// <returns> + /// true if the object that implements <see cref="IFormCollection" /> contains + /// an element with the specified key; otherwise, false. + /// </returns> + /// <exception cref="System.ArgumentNullException"> + /// key is null. + /// </exception> + bool TryGetValue(string key, out StringValues value); + + /// <summary> + /// Gets the value with the specified key. + /// </summary> + /// <param name="key"> + /// The key of the value to get. + /// </param> + /// <returns> + /// The element with the specified key, or <c>StringValues.Empty</c> if the key is not present. + /// </returns> + /// <exception cref="System.ArgumentNullException"> + /// key is null. + /// </exception> + /// <remarks> + /// <see cref="IFormCollection" /> has a different indexer contract than + /// <see cref="IDictionary{TKey, TValue}" />, as it will return <c>StringValues.Empty</c> for missing entries + /// rather than throwing an Exception. + /// </remarks> + StringValues this[string key] { get; } + + /// <summary> + /// The file collection sent with the request. + /// </summary> + /// <returns>The files included with the request.</returns> + IFormFileCollection Files { get; } + } +} diff --git a/src/Http/Http.Features/src/IFormFeature.cs b/src/Http/Http.Features/src/IFormFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..f10ed47b806ef7f00408c7aa13a9336d9b673789 --- /dev/null +++ b/src/Http/Http.Features/src/IFormFeature.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IFormFeature + { + /// <summary> + /// Indicates if the request has a supported form content-type. + /// </summary> + bool HasFormContentType { get; } + + /// <summary> + /// The parsed form, if any. + /// </summary> + IFormCollection Form { get; set; } + + /// <summary> + /// Parses the request body as a form. + /// </summary> + /// <returns></returns> + IFormCollection ReadForm(); + + /// <summary> + /// Parses the request body as a form. + /// </summary> + /// <param name="cancellationToken"></param> + /// <returns></returns> + Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Http/Http.Features/src/IFormFile.cs b/src/Http/Http.Features/src/IFormFile.cs new file mode 100644 index 0000000000000000000000000000000000000000..f52e71bfee16c24e14182f05d6c3a0f2b05f64cb --- /dev/null +++ b/src/Http/Http.Features/src/IFormFile.cs @@ -0,0 +1,63 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Represents a file sent with the HttpRequest. + /// </summary> + public interface IFormFile + { + /// <summary> + /// Gets the raw Content-Type header of the uploaded file. + /// </summary> + string ContentType { get; } + + /// <summary> + /// Gets the raw Content-Disposition header of the uploaded file. + /// </summary> + string ContentDisposition { get; } + + /// <summary> + /// Gets the header dictionary of the uploaded file. + /// </summary> + IHeaderDictionary Headers { get; } + + /// <summary> + /// Gets the file length in bytes. + /// </summary> + long Length { get; } + + /// <summary> + /// Gets the form field name from the Content-Disposition header. + /// </summary> + string Name { get; } + + /// <summary> + /// Gets the file name from the Content-Disposition header. + /// </summary> + string FileName { get; } + + /// <summary> + /// Opens the request stream for reading the uploaded file. + /// </summary> + Stream OpenReadStream(); + + /// <summary> + /// Copies the contents of the uploaded file to the <paramref name="target"/> stream. + /// </summary> + /// <param name="target">The stream to copy the file contents to.</param> + void CopyTo(Stream target); + + /// <summary> + /// Asynchronously copies the contents of the uploaded file to the <paramref name="target"/> stream. + /// </summary> + /// <param name="target">The stream to copy the file contents to.</param> + /// <param name="cancellationToken"></param> + Task CopyToAsync(Stream target, CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/src/Http/Http.Features/src/IFormFileCollection.cs b/src/Http/Http.Features/src/IFormFileCollection.cs new file mode 100644 index 0000000000000000000000000000000000000000..e66c96e05ddcd777c43422fa7a74ee700f9fdcb1 --- /dev/null +++ b/src/Http/Http.Features/src/IFormFileCollection.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Represents the collection of files sent with the HttpRequest. + /// </summary> + public interface IFormFileCollection : IReadOnlyList<IFormFile> + { + IFormFile this[string name] { get; } + + IFormFile GetFile(string name); + + IReadOnlyList<IFormFile> GetFiles(string name); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IHeaderDictionary.cs b/src/Http/Http.Features/src/IHeaderDictionary.cs new file mode 100644 index 0000000000000000000000000000000000000000..dfde3f33e31e1de2af23f0d0b8d8606373fa7126 --- /dev/null +++ b/src/Http/Http.Features/src/IHeaderDictionary.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Represents HttpRequest and HttpResponse headers + /// </summary> + public interface IHeaderDictionary : IDictionary<string, StringValues> + { + /// <summary> + /// IHeaderDictionary has a different indexer contract than IDictionary, where it will return StringValues.Empty for missing entries. + /// </summary> + /// <param name="key"></param> + /// <returns>The stored value, or StringValues.Empty if the key is not present.</returns> + new StringValues this[string key] { get; set; } + + /// <summary> + /// Strongly typed access to the Content-Length header. Implementations must keep this in sync with the string representation. + /// </summary> + long? ContentLength { get; set; } + } +} diff --git a/src/Http/Http.Features/src/IHttpBodyControlFeature.cs b/src/Http/Http.Features/src/IHttpBodyControlFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..3f61be97882ca897897eab25632527f5cfd5e268 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpBodyControlFeature.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features +{ + /// <summary> + /// Controls the IO behavior for the <see cref="IHttpRequestFeature.Body"/> and <see cref="IHttpResponseFeature.Body"/> + /// </summary> + public interface IHttpBodyControlFeature + { + /// <summary> + /// Gets or sets a value that controls whether synchronous IO is allowed for the <see cref="IHttpRequestFeature.Body"/> and <see cref="IHttpResponseFeature.Body"/> + /// </summary> + bool AllowSynchronousIO { get; set; } + } +} diff --git a/src/Http/Http.Features/src/IHttpBufferingFeature.cs b/src/Http/Http.Features/src/IHttpBufferingFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..fae7f3d0ffd3e7bbd4d550447511d4c9241ebf68 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpBufferingFeature.cs @@ -0,0 +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. + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IHttpBufferingFeature + { + void DisableRequestBuffering(); + void DisableResponseBuffering(); + } +} diff --git a/src/Http/Http.Features/src/IHttpConnectionFeature.cs b/src/Http/Http.Features/src/IHttpConnectionFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..932e9bfe2c1c408c9ab91ae37960ddd3816d76c6 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpConnectionFeature.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// <summary> + /// Information regarding the TCP/IP connection carrying the request. + /// </summary> + public interface IHttpConnectionFeature + { + /// <summary> + /// The unique identifier for the connection the request was received on. This is primarily for diagnostic purposes. + /// </summary> + string ConnectionId { get; set; } + + /// <summary> + /// The IPAddress of the client making the request. Note this may be for a proxy rather than the end user. + /// </summary> + IPAddress RemoteIpAddress { get; set; } + + /// <summary> + /// The local IPAddress on which the request was received. + /// </summary> + IPAddress LocalIpAddress { get; set; } + + /// <summary> + /// The remote port of the client making the request. + /// </summary> + int RemotePort { get; set; } + + /// <summary> + /// The local port on which the request was received. + /// </summary> + int LocalPort { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IHttpMaxRequestBodySizeFeature.cs b/src/Http/Http.Features/src/IHttpMaxRequestBodySizeFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..c02000c72afa3809a784b8d6a64982702d2dd136 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpMaxRequestBodySizeFeature.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features +{ + /// <summary> + /// Feature to inspect and modify the maximum request body size for a single request. + /// </summary> + public interface IHttpMaxRequestBodySizeFeature + { + /// <summary> + /// Indicates whether <see cref="MaxRequestBodySize"/> is read-only. + /// If true, this could mean that the request body has already been read from + /// or that <see cref="IHttpUpgradeFeature.UpgradeAsync"/> was called. + /// </summary> + bool IsReadOnly { get; } + + /// <summary> + /// The maximum allowed size of the current request body in bytes. + /// When set to null, the maximum request body size is unlimited. + /// This cannot be modified after the reading the request body has started. + /// This limit does not affect upgraded connections which are always unlimited. + /// </summary> + /// <remarks> + /// Defaults to the server's global max request body size limit. + /// </remarks> + long? MaxRequestBodySize { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IHttpRequestFeature.cs b/src/Http/Http.Features/src/IHttpRequestFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..5a84221b5756f271a15095a07fd511994c90e734 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpRequestFeature.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// <summary> + /// Contains the details of a given request. These properties should all be mutable. + /// None of these properties should ever be set to null. + /// </summary> + public interface IHttpRequestFeature + { + /// <summary> + /// The HTTP-version as defined in RFC 7230. E.g. "HTTP/1.1" + /// </summary> + string Protocol { get; set; } + + /// <summary> + /// The request uri scheme. E.g. "http" or "https". Note this value is not included + /// in the original request, it is inferred by checking if the transport used a TLS + /// connection or not. + /// </summary> + string Scheme { get; set; } + + /// <summary> + /// The request method as defined in RFC 7230. E.g. "GET", "HEAD", "POST", etc.. + /// </summary> + string Method { get; set; } + + /// <summary> + /// The first portion of the request path associated with application root. The value + /// is un-escaped. The value may be string.Empty. + /// </summary> + string PathBase { get; set; } + + /// <summary> + /// The portion of the request path that identifies the requested resource. The value + /// is un-escaped. The value may be string.Empty if <see cref="PathBase"/> contains the + /// full path. + /// </summary> + string Path { get; set; } + + /// <summary> + /// The query portion of the request-target as defined in RFC 7230. The value + /// may be string.Empty. If not empty then the leading '?' will be included. The value + /// is in its original form, without un-escaping. + /// </summary> + string QueryString { get; set; } + + /// <summary> + /// The request target as it was sent in the HTTP request. This property contains the + /// raw path and full query, as well as other request targets such as * for OPTIONS + /// requests (https://tools.ietf.org/html/rfc7230#section-5.3). + /// </summary> + /// <remarks> + /// This property is not used internally for routing or authorization decisions. It has not + /// been UrlDecoded and care should be taken in its use. + /// </remarks> + string RawTarget { get; set; } + + /// <summary> + /// Headers included in the request, aggregated by header name. The values are not split + /// or merged across header lines. E.g. The following headers: + /// HeaderA: value1, value2 + /// HeaderA: value3 + /// Result in Headers["HeaderA"] = { "value1, value2", "value3" } + /// </summary> + IHeaderDictionary Headers { get; set; } + + /// <summary> + /// A <see cref="Stream"/> representing the request body, if any. Stream.Null may be used + /// to represent an empty request body. + /// </summary> + Stream Body { get; set; } + } +} diff --git a/src/Http/Http.Features/src/IHttpRequestIdentifierFeature.cs b/src/Http/Http.Features/src/IHttpRequestIdentifierFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..9b0b5201d7df1e184f5ecc025b90a86f99f7d146 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpRequestIdentifierFeature.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// <summary> + /// Feature to identify a request. + /// </summary> + public interface IHttpRequestIdentifierFeature + { + /// <summary> + /// Identifier to trace a request. + /// </summary> + string TraceIdentifier { get; set; } + } +} diff --git a/src/Http/Http.Features/src/IHttpRequestLifetimeFeature.cs b/src/Http/Http.Features/src/IHttpRequestLifetimeFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..1bdac15766cc8246453d0740769c1106c0315141 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpRequestLifetimeFeature.cs @@ -0,0 +1,23 @@ +// 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; + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IHttpRequestLifetimeFeature + { + /// <summary> + /// A <see cref="CancellationToken"/> that fires if the request is aborted and + /// the application should cease processing. The token will not fire if the request + /// completes successfully. + /// </summary> + CancellationToken RequestAborted { get; set; } + + /// <summary> + /// Forcefully aborts the request if it has not already completed. This will result in + /// RequestAborted being triggered. + /// </summary> + void Abort(); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IHttpResponseFeature.cs b/src/Http/Http.Features/src/IHttpResponseFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..9d3b957efb8443ab25cc0dac023502991344cb5b --- /dev/null +++ b/src/Http/Http.Features/src/IHttpResponseFeature.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// <summary> + /// Represents the fields and state of an HTTP response. + /// </summary> + public interface IHttpResponseFeature + { + /// <summary> + /// The status-code as defined in RFC 7230. The default value is 200. + /// </summary> + int StatusCode { get; set; } + + /// <summary> + /// The reason-phrase as defined in RFC 7230. Note this field is no longer supported by HTTP/2. + /// </summary> + string ReasonPhrase { get; set; } + + /// <summary> + /// The response headers to send. Headers with multiple values will be emitted as multiple headers. + /// </summary> + IHeaderDictionary Headers { get; set; } + + /// <summary> + /// The <see cref="Stream"/> for writing the response body. + /// </summary> + Stream Body { get; set; } + + /// <summary> + /// Indicates if the response has started. If true, the <see cref="StatusCode"/>, + /// <see cref="ReasonPhrase"/>, and <see cref="Headers"/> are now immutable, and + /// OnStarting should no longer be called. + /// </summary> + bool HasStarted { get; } + + /// <summary> + /// Registers a callback to be invoked just before the response starts. This is the + /// last chance to modify the <see cref="Headers"/>, <see cref="StatusCode"/>, or + /// <see cref="ReasonPhrase"/>. + /// </summary> + /// <param name="callback">The callback to invoke when starting the response.</param> + /// <param name="state">The state to pass into the callback.</param> + void OnStarting(Func<object, Task> callback, object state); + + /// <summary> + /// Registers a callback to be invoked after a response has fully completed. This is + /// intended for resource cleanup. + /// </summary> + /// <param name="callback">The callback to invoke after the response has completed.</param> + /// <param name="state">The state to pass into the callback.</param> + void OnCompleted(Func<object, Task> callback, object state); + } +} diff --git a/src/Http/Http.Features/src/IHttpSendFileFeature.cs b/src/Http/Http.Features/src/IHttpSendFileFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..1e2684130fa1a25ae4e311b0b77471a4dee661d1 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpSendFileFeature.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// <summary> + /// Provides an efficient mechanism for transferring files from disk to the network. + /// </summary> + public interface IHttpSendFileFeature + { + /// <summary> + /// Sends the requested file in the response body. This may bypass the IHttpResponseFeature.Body + /// <see cref="Stream"/>. A response may include multiple writes. + /// </summary> + /// <param name="path">The full disk path to the file.</param> + /// <param name="offset">The offset in the file to start at.</param> + /// <param name="count">The number of bytes to send, or null to send the remainder of the file.</param> + /// <param name="cancellation">A <see cref="CancellationToken"/> used to abort the transmission.</param> + /// <returns></returns> + Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IHttpUpgradeFeature.cs b/src/Http/Http.Features/src/IHttpUpgradeFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..e434fe0b97c61f286c45cccb484f77d640fc1744 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpUpgradeFeature.cs @@ -0,0 +1,24 @@ +// 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.IO; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IHttpUpgradeFeature + { + /// <summary> + /// Indicates if the server can upgrade this request to an opaque, bidirectional stream. + /// </summary> + bool IsUpgradableRequest { get; } + + /// <summary> + /// Attempt to upgrade the request to an opaque, bidirectional stream. The response status code + /// and headers need to be set before this is invoked. Check <see cref="IsUpgradableRequest"/> + /// before invoking. + /// </summary> + /// <returns></returns> + Task<Stream> UpgradeAsync(); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IHttpWebSocketFeature.cs b/src/Http/Http.Features/src/IHttpWebSocketFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..c1d116126a5f194cfcfa0cfaebb7b1bc8b6c776a --- /dev/null +++ b/src/Http/Http.Features/src/IHttpWebSocketFeature.cs @@ -0,0 +1,24 @@ +// 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.Net.WebSockets; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IHttpWebSocketFeature + { + /// <summary> + /// Indicates if this is a WebSocket upgrade request. + /// </summary> + bool IsWebSocketRequest { get; } + + /// <summary> + /// Attempts to upgrade the request to a <see cref="WebSocket"/>. Check <see cref="IsWebSocketRequest"/> + /// before invoking this. + /// </summary> + /// <param name="context"></param> + /// <returns></returns> + Task<WebSocket> AcceptAsync(WebSocketAcceptContext context); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IItemsFeature.cs b/src/Http/Http.Features/src/IItemsFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..bea03e466c1b7be18622e41727e13ec77d699c28 --- /dev/null +++ b/src/Http/Http.Features/src/IItemsFeature.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IItemsFeature + { + IDictionary<object, object> Items { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IQueryCollection.cs b/src/Http/Http.Features/src/IQueryCollection.cs new file mode 100644 index 0000000000000000000000000000000000000000..5d45ad2493604e697c6cc84083c014cbda49c8a8 --- /dev/null +++ b/src/Http/Http.Features/src/IQueryCollection.cs @@ -0,0 +1,88 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Represents the HttpRequest query string collection + /// </summary> + public interface IQueryCollection : IEnumerable<KeyValuePair<string, StringValues>> + { + /// <summary> + /// Gets the number of elements contained in the <see cref="IQueryCollection" />. + /// </summary> + /// <returns> + /// The number of elements contained in the <see cref="IQueryCollection" />. + /// </returns> + int Count { get; } + + /// <summary> + /// Gets an <see cref="ICollection{T}" /> containing the keys of the + /// <see cref="IQueryCollection" />. + /// </summary> + /// <returns> + /// An <see cref="ICollection{T}" /> containing the keys of the object + /// that implements <see cref="IQueryCollection" />. + /// </returns> + ICollection<string> Keys { get; } + + /// <summary> + /// Determines whether the <see cref="IQueryCollection" /> contains an element + /// with the specified key. + /// </summary> + /// <param name="key"> + /// The key to locate in the <see cref="IQueryCollection" />. + /// </param> + /// <returns> + /// true if the <see cref="IQueryCollection" /> contains an element with + /// the key; otherwise, false. + /// </returns> + /// <exception cref="System.ArgumentNullException"> + /// key is null. + /// </exception> + bool ContainsKey(string key); + + /// <summary> + /// Gets the value associated with the specified key. + /// </summary> + /// <param name="key"> + /// The key of the value to get. + /// </param> + /// <param name="value"> + /// The key of the value to get. + /// When this method returns, the value associated with the specified key, if the + /// key is found; otherwise, the default value for the type of the value parameter. + /// This parameter is passed uninitialized. + /// </param> + /// <returns> + /// true if the object that implements <see cref="IQueryCollection" /> contains + /// an element with the specified key; otherwise, false. + /// </returns> + /// <exception cref="System.ArgumentNullException"> + /// key is null. + /// </exception> + bool TryGetValue(string key, out StringValues value); + + /// <summary> + /// Gets the value with the specified key. + /// </summary> + /// <param name="key"> + /// The key of the value to get. + /// </param> + /// <returns> + /// The element with the specified key, or <c>StringValues.Empty</c> if the key is not present. + /// </returns> + /// <exception cref="System.ArgumentNullException"> + /// key is null. + /// </exception> + /// <remarks> + /// <see cref="IQueryCollection" /> has a different indexer contract than + /// <see cref="IDictionary{TKey, TValue}" />, as it will return <c>StringValues.Empty</c> for missing entries + /// rather than throwing an Exception. + /// </remarks> + StringValues this[string key] { get; } + } +} diff --git a/src/Http/Http.Features/src/IQueryFeature.cs b/src/Http/Http.Features/src/IQueryFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..4f307f8f9017bb77db1ecfa84ed723743f6c2153 --- /dev/null +++ b/src/Http/Http.Features/src/IQueryFeature.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IQueryFeature + { + IQueryCollection Query { get; set; } + } +} diff --git a/src/Http/Http.Features/src/IRequestCookieCollection.cs b/src/Http/Http.Features/src/IRequestCookieCollection.cs new file mode 100644 index 0000000000000000000000000000000000000000..6e9444ac8f0d90044532ed6da2ed9acec0836c3c --- /dev/null +++ b/src/Http/Http.Features/src/IRequestCookieCollection.cs @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Represents the HttpRequest cookie collection + /// </summary> + public interface IRequestCookieCollection : IEnumerable<KeyValuePair<string, string>> + { + /// <summary> + /// Gets the number of elements contained in the <see cref="IRequestCookieCollection" />. + /// </summary> + /// <returns> + /// The number of elements contained in the <see cref="IRequestCookieCollection" />. + /// </returns> + int Count { get; } + + /// <summary> + /// Gets an <see cref="ICollection{T}" /> containing the keys of the + /// <see cref="IRequestCookieCollection" />. + /// </summary> + /// <returns> + /// An <see cref="ICollection{T}" /> containing the keys of the object + /// that implements <see cref="IRequestCookieCollection" />. + /// </returns> + ICollection<string> Keys { get; } + + /// <summary> + /// Determines whether the <see cref="IRequestCookieCollection" /> contains an element + /// with the specified key. + /// </summary> + /// <param name="key"> + /// The key to locate in the <see cref="IRequestCookieCollection" />. + /// </param> + /// <returns> + /// true if the <see cref="IRequestCookieCollection" /> contains an element with + /// the key; otherwise, false. + /// </returns> + /// <exception cref="System.ArgumentNullException"> + /// key is null. + /// </exception> + bool ContainsKey(string key); + + /// <summary> + /// Gets the value associated with the specified key. + /// </summary> + /// <param name="key"> + /// The key of the value to get. + /// </param> + /// <param name="value"> + /// The key of the value to get. + /// When this method returns, the value associated with the specified key, if the + /// key is found; otherwise, the default value for the type of the value parameter. + /// This parameter is passed uninitialized. + /// </param> + /// <returns> + /// true if the object that implements <see cref="IRequestCookieCollection" /> contains + /// an element with the specified key; otherwise, false. + /// </returns> + /// <exception cref="System.ArgumentNullException"> + /// key is null. + /// </exception> + bool TryGetValue(string key, out string value); + + /// <summary> + /// Gets the value with the specified key. + /// </summary> + /// <param name="key"> + /// The key of the value to get. + /// </param> + /// <returns> + /// The element with the specified key, or <c>string.Empty</c> if the key is not present. + /// </returns> + /// <exception cref="System.ArgumentNullException"> + /// key is null. + /// </exception> + /// <remarks> + /// <see cref="IRequestCookieCollection" /> has a different indexer contract than + /// <see cref="IDictionary{TKey, TValue}" />, as it will return <c>string.Empty</c> for missing entries + /// rather than throwing an Exception. + /// </remarks> + string this[string key] { get; } + } +} diff --git a/src/Http/Http.Features/src/IRequestCookiesFeature.cs b/src/Http/Http.Features/src/IRequestCookiesFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..55ba6036423899b4f92dba886413e2203f11a5a7 --- /dev/null +++ b/src/Http/Http.Features/src/IRequestCookiesFeature.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IRequestCookiesFeature + { + IRequestCookieCollection Cookies { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IResponseCookies.cs b/src/Http/Http.Features/src/IResponseCookies.cs new file mode 100644 index 0000000000000000000000000000000000000000..9c8c3b42bab90a0e50947ecded06233c2d515f2b --- /dev/null +++ b/src/Http/Http.Features/src/IResponseCookies.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// A wrapper for the response Set-Cookie header. + /// </summary> + public interface IResponseCookies + { + /// <summary> + /// Add a new cookie and value. + /// </summary> + /// <param name="key">Name of the new cookie.</param> + /// <param name="value">Value of the new cookie.</param> + void Append(string key, string value); + + /// <summary> + /// Add a new cookie. + /// </summary> + /// <param name="key">Name of the new cookie.</param> + /// <param name="value">Value of the new cookie.</param> + /// <param name="options"><see cref="CookieOptions"/> included in the new cookie setting.</param> + void Append(string key, string value, CookieOptions options); + + /// <summary> + /// Sets an expired cookie. + /// </summary> + /// <param name="key">Name of the cookie to expire.</param> + void Delete(string key); + + /// <summary> + /// Sets an expired cookie. + /// </summary> + /// <param name="key">Name of the cookie to expire.</param> + /// <param name="options"> + /// <see cref="CookieOptions"/> used to discriminate the particular cookie to expire. The + /// <see cref="CookieOptions.Domain"/> and <see cref="CookieOptions.Path"/> values are especially important. + /// </param> + void Delete(string key, CookieOptions options); + } +} diff --git a/src/Http/Http.Features/src/IResponseCookiesFeature.cs b/src/Http/Http.Features/src/IResponseCookiesFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..7ce10418400d3416525d2b710d32c9ee0a11346c --- /dev/null +++ b/src/Http/Http.Features/src/IResponseCookiesFeature.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features +{ + /// <summary> + /// A helper for creating the response Set-Cookie header. + /// </summary> + public interface IResponseCookiesFeature + { + /// <summary> + /// Gets the wrapper for the response Set-Cookie header. + /// </summary> + IResponseCookies Cookies { get; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IServiceProvidersFeature.cs b/src/Http/Http.Features/src/IServiceProvidersFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..aed0fc91defc3a32d2a89094fb6ff65f1135da5e --- /dev/null +++ b/src/Http/Http.Features/src/IServiceProvidersFeature.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IServiceProvidersFeature + { + IServiceProvider RequestServices { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/ISession.cs b/src/Http/Http.Features/src/ISession.cs new file mode 100644 index 0000000000000000000000000000000000000000..6bd780684d1db65228cc9ed13df0c638ad8d162b --- /dev/null +++ b/src/Http/Http.Features/src/ISession.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + public interface ISession + { + /// <summary> + /// Indicate whether the current session has loaded. + /// </summary> + bool IsAvailable { get; } + + /// <summary> + /// A unique identifier for the current session. This is not the same as the session cookie + /// since the cookie lifetime may not be the same as the session entry lifetime in the data store. + /// </summary> + string Id { get; } + + /// <summary> + /// Enumerates all the keys, if any. + /// </summary> + IEnumerable<string> Keys { get; } + + /// <summary> + /// Load the session from the data store. This may throw if the data store is unavailable. + /// </summary> + /// <returns></returns> + Task LoadAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// <summary> + /// Store the session in the data store. This may throw if the data store is unavailable. + /// </summary> + /// <returns></returns> + Task CommitAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// <summary> + /// Retrieve the value of the given key, if present. + /// </summary> + /// <param name="key"></param> + /// <param name="value"></param> + /// <returns></returns> + bool TryGetValue(string key, out byte[] value); + + /// <summary> + /// Set the given key and value in the current session. This will throw if the session + /// was not established prior to sending the response. + /// </summary> + /// <param name="key"></param> + /// <param name="value"></param> + void Set(string key, byte[] value); + + /// <summary> + /// Remove the given key from the session if present. + /// </summary> + /// <param name="key"></param> + void Remove(string key); + + /// <summary> + /// Remove all entries from the current session, if any. + /// The session cookie is not removed. + /// </summary> + void Clear(); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/ISessionFeature.cs b/src/Http/Http.Features/src/ISessionFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..23652994158300c819349b68a0173e5108c5abb1 --- /dev/null +++ b/src/Http/Http.Features/src/ISessionFeature.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface ISessionFeature + { + ISession Session { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/ITlsConnectionFeature.cs b/src/Http/Http.Features/src/ITlsConnectionFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..c34a3339d5293bd0cf0ed881d7113b1d192eaa2d --- /dev/null +++ b/src/Http/Http.Features/src/ITlsConnectionFeature.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface ITlsConnectionFeature + { + /// <summary> + /// Synchronously retrieves the client certificate, if any. + /// </summary> + X509Certificate2 ClientCertificate { get; set; } + + /// <summary> + /// Asynchronously retrieves the client certificate, if any. + /// </summary> + /// <returns></returns> + Task<X509Certificate2> GetClientCertificateAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Http/Http.Features/src/ITlsTokenBindingFeature.cs b/src/Http/Http.Features/src/ITlsTokenBindingFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..d63333dd0a839003ec170a0910b3e72508467537 --- /dev/null +++ b/src/Http/Http.Features/src/ITlsTokenBindingFeature.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features +{ + /// <summary> + /// Provides information regarding TLS token binding parameters. + /// </summary> + /// <remarks> + /// TLS token bindings help mitigate the risk of impersonation by an attacker in the + /// event an authenticated client's bearer tokens are somehow exfiltrated from the + /// client's machine. See https://datatracker.ietf.org/doc/draft-popov-token-binding/ + /// for more information. + /// </remarks> + public interface ITlsTokenBindingFeature + { + /// <summary> + /// Gets the 'provided' token binding identifier associated with the request. + /// </summary> + /// <returns>The token binding identifier, or null if the client did not + /// supply a 'provided' token binding or valid proof of possession of the + /// associated private key. The caller should treat this identifier as an + /// opaque blob and should not try to parse it.</returns> + byte[] GetProvidedTokenBindingId(); + + /// <summary> + /// Gets the 'referred' token binding identifier associated with the request. + /// </summary> + /// <returns>The token binding identifier, or null if the client did not + /// supply a 'referred' token binding or valid proof of possession of the + /// associated private key. The caller should treat this identifier as an + /// opaque blob and should not try to parse it.</returns> + byte[] GetReferredTokenBindingId(); + } +} diff --git a/src/Http/Http.Features/src/ITrackingConsentFeature.cs b/src/Http/Http.Features/src/ITrackingConsentFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..e7fbeaeaf338cba8b5a742eb5355f694775f27a2 --- /dev/null +++ b/src/Http/Http.Features/src/ITrackingConsentFeature.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features +{ + /// <summary> + /// Used to query, grant, and withdraw user consent regarding the storage of user + /// information related to site activity and functionality. + /// </summary> + public interface ITrackingConsentFeature + { + /// <summary> + /// Indicates if consent is required for the given request. + /// </summary> + bool IsConsentNeeded { get; } + + /// <summary> + /// Indicates if consent was given. + /// </summary> + bool HasConsent { get; } + + /// <summary> + /// Indicates either if consent has been given or if consent is not required. + /// </summary> + bool CanTrack { get; } + + /// <summary> + /// Grants consent for this request. If the response has not yet started then + /// this will also grant consent for future requests. + /// </summary> + void GrantConsent(); + + /// <summary> + /// Withdraws consent for this request. If the response has not yet started then + /// this will also withdraw consent for future requests. + /// </summary> + void WithdrawConsent(); + + /// <summary> + /// Creates a consent cookie for use when granting consent from a javascript client. + /// </summary> + string CreateConsentCookie(); + } +} diff --git a/src/Http/Http.Features/src/Microsoft.AspNetCore.Http.Features.csproj b/src/Http/Http.Features/src/Microsoft.AspNetCore.Http.Features.csproj new file mode 100644 index 0000000000000000000000000000000000000000..7a2310a6fd16f9e496ada2288d45a20575049345 --- /dev/null +++ b/src/Http/Http.Features/src/Microsoft.AspNetCore.Http.Features.csproj @@ -0,0 +1,15 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>ASP.NET Core HTTP feature interface definitions.</Description> + <TargetFramework>netstandard2.0</TargetFramework> + <NoWarn>$(NoWarn);CS1591</NoWarn> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore</PackageTags> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.Extensions.Primitives" /> + </ItemGroup> + +</Project> diff --git a/src/Http/Http.Features/src/SameSiteMode.cs b/src/Http/Http.Features/src/SameSiteMode.cs new file mode 100644 index 0000000000000000000000000000000000000000..0ae4481e3d241162ed6d14d67ab84c9e721640d3 --- /dev/null +++ b/src/Http/Http.Features/src/SameSiteMode.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http +{ + // RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 + // This mirrors Microsoft.Net.Http.Headers.SameSiteMode + public enum SameSiteMode + { + None = 0, + Lax, + Strict + } +} diff --git a/src/Http/Http.Features/src/WebSocketAcceptContext.cs b/src/Http/Http.Features/src/WebSocketAcceptContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..5e3659d6478413c9588005c2fdd0bef39247e168 --- /dev/null +++ b/src/Http/Http.Features/src/WebSocketAcceptContext.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http +{ + public class WebSocketAcceptContext + { + public virtual string SubProtocol { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/baseline.netcore.json b/src/Http/Http.Features/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..6af2ceccf9c00029a6aa1278e7a695bdf6f011a0 --- /dev/null +++ b/src/Http/Http.Features/src/baseline.netcore.json @@ -0,0 +1,2727 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Http.Features, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Http.CookieOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Domain", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Domain", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Path", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Expires", + "Parameters": [], + "ReturnType": "System.Nullable<System.DateTimeOffset>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Expires", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.DateTimeOffset>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Secure", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Secure", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SameSite", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.SameSiteMode", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SameSite", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.SameSiteMode" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HttpOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HttpOnly", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxAge", + "Parameters": [], + "ReturnType": "System.Nullable<System.TimeSpan>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.TimeSpan>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsEssential", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsEssential", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IFormCollection", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Count", + "Parameters": [], + "ReturnType": "System.Int32", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Keys", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection<System.String>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ContainsKey", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetValue", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringValues", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Files", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IFormFileCollection", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IFormFile", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ContentType", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentDisposition", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Length", + "Parameters": [], + "ReturnType": "System.Int64", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FileName", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OpenReadStream", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CopyTo", + "Parameters": [ + { + "Name": "target", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CopyToAsync", + "Parameters": [ + { + "Name": "target", + "Type": "System.IO.Stream" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IFormFileCollection", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.IFormFile>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.IFormFile", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetFile", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.IFormFile", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetFiles", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.IFormFile>", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IDictionary<System.String, Microsoft.Extensions.Primitives.StringValues>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringValues", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentLength", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.Int64>" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IQueryCollection", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Count", + "Parameters": [], + "ReturnType": "System.Int32", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Keys", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection<System.String>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ContainsKey", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetValue", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringValues", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IRequestCookieCollection", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.String, System.String>>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Count", + "Parameters": [], + "ReturnType": "System.Int32", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Keys", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection<System.String>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ContainsKey", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetValue", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IResponseCookies", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Append", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Append", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Delete", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Delete", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.ISession", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsAvailable", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Id", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Keys", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable<System.String>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "LoadAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CommitAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetValue", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Byte[]", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Set", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Byte[]" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Clear", + "Parameters": [], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.SameSiteMode", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "None", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "Lax", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + }, + { + "Kind": "Field", + "Name": "Strict", + "Parameters": [], + "GenericParameter": [], + "Literal": "2" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.WebSocketAcceptContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_SubProtocol", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SubProtocol", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.FeatureCollection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<System.Type, System.Object>>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.Type, System.Object>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Revision", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsReadOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + } + ], + "ReturnType": "System.Object", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Get<T0>", + "Parameters": [], + "ReturnType": "T0", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Set<T0>", + "Parameters": [ + { + "Name": "instance", + "Type": "T0" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "defaults", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.FeatureReference<T0>", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Fetch", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Update", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + }, + { + "Name": "feature", + "Type": "T0" + } + ], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Default", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.FeatureReference<T0>", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.FeatureReferences<T0>", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Collection", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Revision", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fetch<T0, T1>", + "Parameters": [ + { + "Name": "cached", + "Type": "T0", + "Direction": "Ref" + }, + { + "Name": "state", + "Type": "T1" + }, + { + "Name": "factory", + "Type": "System.Func<T1, T0>" + } + ], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "Class": true, + "BaseTypeOrInterfaces": [] + }, + { + "ParameterName": "TState", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Fetch<T0>", + "Parameters": [ + { + "Name": "cached", + "Type": "T0", + "Direction": "Ref" + }, + { + "Name": "factory", + "Type": "System.Func<Microsoft.AspNetCore.Http.Features.IFeatureCollection, T0>" + } + ], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "Class": true, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Cache", + "Parameters": [], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TCache", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.Type, System.Object>>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsReadOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Revision", + "Parameters": [], + "ReturnType": "System.Int32", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + } + ], + "ReturnType": "System.Object", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Get<T0>", + "Parameters": [], + "ReturnType": "T0", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Set<T0>", + "Parameters": [ + { + "Name": "instance", + "Type": "T0" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IFormFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HasFormContentType", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Form", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IFormCollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Form", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IFormCollection" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadForm", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IFormCollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadFormAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Http.IFormCollection>", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AllowSynchronousIO", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AllowSynchronousIO", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpBufferingFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "DisableRequestBuffering", + "Parameters": [], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DisableResponseBuffering", + "Parameters": [], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ConnectionId", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConnectionId", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemoteIpAddress", + "Parameters": [], + "ReturnType": "System.Net.IPAddress", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemoteIpAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.IPAddress" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LocalIpAddress", + "Parameters": [], + "ReturnType": "System.Net.IPAddress", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LocalIpAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.IPAddress" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemotePort", + "Parameters": [], + "ReturnType": "System.Int32", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemotePort", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LocalPort", + "Parameters": [], + "ReturnType": "System.Int32", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LocalPort", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsReadOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxRequestBodySize", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxRequestBodySize", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.Int64>" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Protocol", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Protocol", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Scheme", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Scheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Method", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Method", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PathBase", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PathBase", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Path", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_QueryString", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_QueryString", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RawTarget", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RawTarget", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Headers", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Body", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Body", + "Parameters": [ + { + "Name": "value", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_TraceIdentifier", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TraceIdentifier", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequestAborted", + "Parameters": [], + "ReturnType": "System.Threading.CancellationToken", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestAborted", + "Parameters": [ + { + "Name": "value", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Abort", + "Parameters": [], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_StatusCode", + "Parameters": [], + "ReturnType": "System.Int32", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StatusCode", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ReasonPhrase", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ReasonPhrase", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Headers", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Body", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Body", + "Parameters": [ + { + "Name": "value", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasStarted", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStarting", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func<System.Object, System.Threading.Tasks.Task>" + }, + { + "Name": "state", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnCompleted", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func<System.Object, System.Threading.Tasks.Task>" + }, + { + "Name": "state", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "SendFileAsync", + "Parameters": [ + { + "Name": "path", + "Type": "System.String" + }, + { + "Name": "offset", + "Type": "System.Int64" + }, + { + "Name": "count", + "Type": "System.Nullable<System.Int64>" + }, + { + "Name": "cancellation", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpUpgradeFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsUpgradableRequest", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UpgradeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<System.IO.Stream>", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsWebSocketRequest", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AcceptAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.WebSocketAcceptContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Net.WebSockets.WebSocket>", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IItemsFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Items", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.Object, System.Object>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Items", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary<System.Object, System.Object>" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IQueryFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Query", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IQueryCollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Query", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IQueryCollection" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IRequestCookiesFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Cookies", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IRequestCookieCollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Cookies", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IRequestCookieCollection" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IResponseCookiesFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Cookies", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IResponseCookies", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IServiceProvidersFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequestServices", + "Parameters": [], + "ReturnType": "System.IServiceProvider", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestServices", + "Parameters": [ + { + "Name": "value", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.ISessionFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Session", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.ISession", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Session", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.ISession" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ClientCertificate", + "Parameters": [], + "ReturnType": "System.Security.Cryptography.X509Certificates.X509Certificate2", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClientCertificate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Cryptography.X509Certificates.X509Certificate2" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetClientCertificateAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Security.Cryptography.X509Certificates.X509Certificate2>", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.ITlsTokenBindingFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetProvidedTokenBindingId", + "Parameters": [], + "ReturnType": "System.Byte[]", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetReferredTokenBindingId", + "Parameters": [], + "ReturnType": "System.Byte[]", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.ITrackingConsentFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsConsentNeeded", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasConsent", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanTrack", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GrantConsent", + "Parameters": [], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WithdrawConsent", + "Parameters": [], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateConsentCookie", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.AuthenticateContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Accepted", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, System.String>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Description", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, System.Object>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Error", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Authenticated", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "System.Collections.Generic.IDictionary<System.String, System.String>" + }, + { + "Name": "description", + "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "NotAuthenticated", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Failed", + "Parameters": [ + { + "Name": "error", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.ChallengeBehavior", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Automatic", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "Unauthorized", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + }, + { + "Kind": "Field", + "Name": "Forbidden", + "Parameters": [], + "GenericParameter": [], + "Literal": "2" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.ChallengeContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Behavior", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.Authentication.ChallengeBehavior", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, System.String>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Accepted", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Accept", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "System.Collections.Generic.IDictionary<System.String, System.String>" + }, + { + "Name": "behavior", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.ChallengeBehavior" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.DescribeSchemesContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Results", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable<System.Collections.Generic.IDictionary<System.String, System.Object>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Accept", + "Parameters": [ + { + "Name": "description", + "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.IAuthenticationHandler", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetDescriptions", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.DescribeSchemesContext" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.AuthenticateContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.ChallengeContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.SignInContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.SignOutContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_User", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Handler", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.Authentication.IAuthenticationHandler", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Handler", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.IAuthenticationHandler" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.SignInContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, System.String>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Accepted", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Accept", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "System.Collections.Generic.IDictionary<System.String, System.String>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.SignOutContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, System.String>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Accepted", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Accept", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "System.Collections.Generic.IDictionary<System.String, System.String>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/Http.Features/test/Authentication/AuthenticateContextTest.cs b/src/Http/Http.Features/test/Authentication/AuthenticateContextTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..c4d901322e584f2563de51dfc80b84547560bc96 --- /dev/null +++ b/src/Http/Http.Features/test/Authentication/AuthenticateContextTest.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; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public class AuthenticateContextTest + { + [Fact] + public void AuthenticateContext_Authenticated() + { + // Arrange + var context = new AuthenticateContext("test"); + + var principal = new ClaimsPrincipal(); + var properties = new Dictionary<string, string>(); + var description = new Dictionary<string, object>(); + + // Act + context.Authenticated(principal, properties, description); + + // Assert + Assert.True(context.Accepted); + Assert.Equal("test", context.AuthenticationScheme); + Assert.Same(description, context.Description); + Assert.Null(context.Error); + Assert.Same(principal, context.Principal); + Assert.Same(properties, context.Properties); + } + + [Fact] + public void AuthenticateContext_Authenticated_SetsUnusedPropertiesToDefault() + { + // Arrange + var context = new AuthenticateContext("test"); + + var principal = new ClaimsPrincipal(); + var properties = new Dictionary<string, string>(); + var description = new Dictionary<string, object>(); + + context.Failed(new Exception()); + + // Act + context.Authenticated(principal, properties, description); + + // Assert + Assert.True(context.Accepted); + Assert.Equal("test", context.AuthenticationScheme); + Assert.Same(description, context.Description); + Assert.Null(context.Error); + Assert.Same(principal, context.Principal); + Assert.Same(properties, context.Properties); + } + + [Fact] + public void AuthenticateContext_Failed() + { + // Arrange + var context = new AuthenticateContext("test"); + + var exception = new Exception(); + + // Act + context.Failed(exception); + + // Assert + Assert.True(context.Accepted); + Assert.Equal("test", context.AuthenticationScheme); + Assert.Null(context.Description); + Assert.Same(exception, context.Error); + Assert.Null(context.Principal); + Assert.Null(context.Properties); + } + + [Fact] + public void AuthenticateContext_Failed_SetsUnusedPropertiesToDefault() + { + // Arrange + var context = new AuthenticateContext("test"); + + var exception = new Exception(); + + context.Authenticated(new ClaimsPrincipal(), new Dictionary<string, string>(), new Dictionary<string, object>()); + + // Act + context.Failed(exception); + + // Assert + Assert.True(context.Accepted); + Assert.Equal("test", context.AuthenticationScheme); + Assert.Null(context.Description); + Assert.Same(exception, context.Error); + Assert.Null(context.Principal); + Assert.Null(context.Properties); + } + + [Fact] + public void AuthenticateContext_NotAuthenticated() + { + // Arrange + var context = new AuthenticateContext("test"); + + // Act + context.NotAuthenticated(); + + // Assert + Assert.True(context.Accepted); + Assert.Equal("test", context.AuthenticationScheme); + Assert.Null(context.Description); + Assert.Null(context.Error); + Assert.Null(context.Principal); + Assert.Null(context.Properties); + } + + [Fact] + public void AuthenticateContext_NotAuthenticated_SetsUnusedPropertiesToDefault_Authenticated() + { + // Arrange + var context = new AuthenticateContext("test"); + + var exception = new Exception(); + + context.Authenticated(new ClaimsPrincipal(), new Dictionary<string, string>(), new Dictionary<string, object>()); + + // Act + context.NotAuthenticated(); + + // Assert + Assert.True(context.Accepted); + Assert.Equal("test", context.AuthenticationScheme); + Assert.Null(context.Description); + Assert.Null(context.Error); + Assert.Null(context.Principal); + Assert.Null(context.Properties); + } + + [Fact] + public void AuthenticateContext_NotAuthenticated_SetsUnusedPropertiesToDefault_Failed() + { + // Arrange + var context = new AuthenticateContext("test"); + + context.Failed(new Exception()); + + context.NotAuthenticated(); + + // Assert + Assert.True(context.Accepted); + Assert.Equal("test", context.AuthenticationScheme); + Assert.Null(context.Description); + Assert.Null(context.Error); + Assert.Null(context.Principal); + Assert.Null(context.Properties); + } + } +} diff --git a/src/Http/Http.Features/test/FeatureCollectionTests.cs b/src/Http/Http.Features/test/FeatureCollectionTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..36ad77f67857670127b98b7f127c5ee5b265320b --- /dev/null +++ b/src/Http/Http.Features/test/FeatureCollectionTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class FeatureCollectionTests + { + [Fact] + public void AddedInterfaceIsReturned() + { + var interfaces = new FeatureCollection(); + var thing = new Thing(); + + interfaces[typeof(IThing)] = thing; + + object thing2 = interfaces[typeof(IThing)]; + Assert.Equal(thing2, thing); + } + + [Fact] + public void IndexerAlsoAddsItems() + { + var interfaces = new FeatureCollection(); + var thing = new Thing(); + + interfaces[typeof(IThing)] = thing; + + Assert.Equal(interfaces[typeof(IThing)], thing); + } + + [Fact] + public void SetNullValueRemoves() + { + var interfaces = new FeatureCollection(); + var thing = new Thing(); + + interfaces[typeof(IThing)] = thing; + Assert.Equal(interfaces[typeof(IThing)], thing); + + interfaces[typeof(IThing)] = null; + + object thing2 = interfaces[typeof(IThing)]; + Assert.Null(thing2); + } + } +} diff --git a/src/Http/Http.Features/test/IThing.cs b/src/Http/Http.Features/test/IThing.cs new file mode 100644 index 0000000000000000000000000000000000000000..f5b0a1e1221a12ed261f6e79f773150e3ba889f7 --- /dev/null +++ b/src/Http/Http.Features/test/IThing.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IThing + { + string Hello(); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..b7c77fc19f2453775e8f79ad0afae87464c9fd71 --- /dev/null +++ b/src/Http/Http.Features/test/Microsoft.AspNetCore.Http.Features.Tests.csproj @@ -0,0 +1,11 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Http.Features" /> + </ItemGroup> + +</Project> diff --git a/src/Http/Http.Features/test/Thing.cs b/src/Http/Http.Features/test/Thing.cs new file mode 100644 index 0000000000000000000000000000000000000000..27a2c0e2857e7c46cf7dff7fc449f9c8ed0e354c --- /dev/null +++ b/src/Http/Http.Features/test/Thing.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features +{ + public class Thing : IThing + { + public string Hello() + { + return "World"; + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Authentication/DefaultAuthenticationManager.cs b/src/Http/Http/src/Authentication/DefaultAuthenticationManager.cs new file mode 100644 index 0000000000000000000000000000000000000000..9f4121f4cb38cda98a1e7aed7f97d3dce837d42f --- /dev/null +++ b/src/Http/Http/src/Authentication/DefaultAuthenticationManager.cs @@ -0,0 +1,184 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; + +namespace Microsoft.AspNetCore.Http.Authentication.Internal +{ + [Obsolete("This is obsolete and will be removed in a future version. See https://go.microsoft.com/fwlink/?linkid=845470.")] + public class DefaultAuthenticationManager : AuthenticationManager + { + // Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func<IFeatureCollection, IHttpAuthenticationFeature> _newAuthenticationFeature = f => new HttpAuthenticationFeature(); + + private HttpContext _context; + private FeatureReferences<IHttpAuthenticationFeature> _features; + + public DefaultAuthenticationManager(HttpContext context) + { + Initialize(context); + } + + public virtual void Initialize(HttpContext context) + { + _context = context; + _features = new FeatureReferences<IHttpAuthenticationFeature>(context.Features); + } + + public virtual void Uninitialize() + { + _features = default(FeatureReferences<IHttpAuthenticationFeature>); + } + + public override HttpContext HttpContext => _context; + + private IHttpAuthenticationFeature HttpAuthenticationFeature => + _features.Fetch(ref _features.Cache, _newAuthenticationFeature); + + public override IEnumerable<AuthenticationDescription> GetAuthenticationSchemes() + { +#pragma warning disable CS0618 // Type or member is obsolete + var handler = HttpAuthenticationFeature.Handler; +#pragma warning restore CS0618 // Type or member is obsolete + if (handler == null) + { + return new AuthenticationDescription[0]; + } + + var describeContext = new DescribeSchemesContext(); + handler.GetDescriptions(describeContext); + return describeContext.Results.Select(description => new AuthenticationDescription(description)); + } + + // Remove once callers have been switched to GetAuthenticateInfoAsync + public override async Task AuthenticateAsync(AuthenticateContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var handler = HttpAuthenticationFeature.Handler; +#pragma warning restore CS0618 // Type or member is obsolete + if (handler != null) + { + await handler.AuthenticateAsync(context); + } + + if (!context.Accepted) + { + throw new InvalidOperationException($"No authentication handler is configured to authenticate for the scheme: {context.AuthenticationScheme}"); + } + } + + public override async Task<AuthenticateInfo> GetAuthenticateInfoAsync(string authenticationScheme) + { + if (authenticationScheme == null) + { + throw new ArgumentNullException(nameof(authenticationScheme)); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var handler = HttpAuthenticationFeature.Handler; +#pragma warning restore CS0618 // Type or member is obsolete + var context = new AuthenticateContext(authenticationScheme); + if (handler != null) + { + await handler.AuthenticateAsync(context); + } + + if (!context.Accepted) + { + throw new InvalidOperationException($"No authentication handler is configured to authenticate for the scheme: {context.AuthenticationScheme}"); + } + + return new AuthenticateInfo + { + Principal = context.Principal, + Properties = new AuthenticationProperties(context.Properties), + Description = new AuthenticationDescription(context.Description) + }; + } + + public override async Task ChallengeAsync(string authenticationScheme, AuthenticationProperties properties, ChallengeBehavior behavior) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var handler = HttpAuthenticationFeature.Handler; +#pragma warning restore CS0618 // Type or member is obsolete + + var challengeContext = new ChallengeContext(authenticationScheme, properties?.Items, behavior); + if (handler != null) + { + await handler.ChallengeAsync(challengeContext); + } + + if (!challengeContext.Accepted) + { + throw new InvalidOperationException($"No authentication handler is configured to handle the scheme: {authenticationScheme}"); + } + } + + public override async Task SignInAsync(string authenticationScheme, ClaimsPrincipal principal, AuthenticationProperties properties) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var handler = HttpAuthenticationFeature.Handler; +#pragma warning restore CS0618 // Type or member is obsolete + + var signInContext = new SignInContext(authenticationScheme, principal, properties?.Items); + if (handler != null) + { + await handler.SignInAsync(signInContext); + } + + if (!signInContext.Accepted) + { + throw new InvalidOperationException($"No authentication handler is configured to handle the scheme: {authenticationScheme}"); + } + } + + public override async Task SignOutAsync(string authenticationScheme, AuthenticationProperties properties) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var handler = HttpAuthenticationFeature.Handler; +#pragma warning restore CS0618 // Type or member is obsolete + + var signOutContext = new SignOutContext(authenticationScheme, properties?.Items); + if (handler != null) + { + await handler.SignOutAsync(signOutContext); + } + + if (!signOutContext.Accepted) + { + throw new InvalidOperationException($"No authentication handler is configured to handle the scheme: {authenticationScheme}"); + } + } + } +} diff --git a/src/Http/Http/src/DefaultHttpContext.cs b/src/Http/Http/src/DefaultHttpContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..d02ad6322bfc46010a616381ab0cf317168459f5 --- /dev/null +++ b/src/Http/Http/src/DefaultHttpContext.cs @@ -0,0 +1,223 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading; +using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.Http.Authentication.Internal; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.AspNetCore.Http.Internal; + +namespace Microsoft.AspNetCore.Http +{ + public class DefaultHttpContext : HttpContext + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func<IFeatureCollection, IItemsFeature> _newItemsFeature = f => new ItemsFeature(); + private readonly static Func<IFeatureCollection, IServiceProvidersFeature> _newServiceProvidersFeature = f => new ServiceProvidersFeature(); + private readonly static Func<IFeatureCollection, IHttpAuthenticationFeature> _newHttpAuthenticationFeature = f => new HttpAuthenticationFeature(); + private readonly static Func<IFeatureCollection, IHttpRequestLifetimeFeature> _newHttpRequestLifetimeFeature = f => new HttpRequestLifetimeFeature(); + private readonly static Func<IFeatureCollection, ISessionFeature> _newSessionFeature = f => new DefaultSessionFeature(); + private readonly static Func<IFeatureCollection, ISessionFeature> _nullSessionFeature = f => null; + private readonly static Func<IFeatureCollection, IHttpRequestIdentifierFeature> _newHttpRequestIdentifierFeature = f => new HttpRequestIdentifierFeature(); + + private FeatureReferences<FeatureInterfaces> _features; + + private HttpRequest _request; + private HttpResponse _response; + +#pragma warning disable CS0618 // Type or member is obsolete + private AuthenticationManager _authenticationManager; +#pragma warning restore CS0618 // Type or member is obsolete + + private ConnectionInfo _connection; + private WebSocketManager _websockets; + + public DefaultHttpContext() + : this(new FeatureCollection()) + { + Features.Set<IHttpRequestFeature>(new HttpRequestFeature()); + Features.Set<IHttpResponseFeature>(new HttpResponseFeature()); + } + + public DefaultHttpContext(IFeatureCollection features) + { + Initialize(features); + } + + public virtual void Initialize(IFeatureCollection features) + { + _features = new FeatureReferences<FeatureInterfaces>(features); + _request = InitializeHttpRequest(); + _response = InitializeHttpResponse(); + } + + public virtual void Uninitialize() + { + _features = default(FeatureReferences<FeatureInterfaces>); + if (_request != null) + { + UninitializeHttpRequest(_request); + _request = null; + } + if (_response != null) + { + UninitializeHttpResponse(_response); + _response = null; + } + if (_authenticationManager != null) + { +#pragma warning disable CS0618 // Type or member is obsolete + UninitializeAuthenticationManager(_authenticationManager); +#pragma warning restore CS0618 // Type or member is obsolete + _authenticationManager = null; + } + if (_connection != null) + { + UninitializeConnectionInfo(_connection); + _connection = null; + } + if (_websockets != null) + { + UninitializeWebSocketManager(_websockets); + _websockets = null; + } + } + + private IItemsFeature ItemsFeature => + _features.Fetch(ref _features.Cache.Items, _newItemsFeature); + + private IServiceProvidersFeature ServiceProvidersFeature => + _features.Fetch(ref _features.Cache.ServiceProviders, _newServiceProvidersFeature); + + private IHttpAuthenticationFeature HttpAuthenticationFeature => + _features.Fetch(ref _features.Cache.Authentication, _newHttpAuthenticationFeature); + + private IHttpRequestLifetimeFeature LifetimeFeature => + _features.Fetch(ref _features.Cache.Lifetime, _newHttpRequestLifetimeFeature); + + private ISessionFeature SessionFeature => + _features.Fetch(ref _features.Cache.Session, _newSessionFeature); + + private ISessionFeature SessionFeatureOrNull => + _features.Fetch(ref _features.Cache.Session, _nullSessionFeature); + + + private IHttpRequestIdentifierFeature RequestIdentifierFeature => + _features.Fetch(ref _features.Cache.RequestIdentifier, _newHttpRequestIdentifierFeature); + + public override IFeatureCollection Features => _features.Collection; + + public override HttpRequest Request => _request; + + public override HttpResponse Response => _response; + + public override ConnectionInfo Connection => _connection ?? (_connection = InitializeConnectionInfo()); + + /// <summary> + /// This is obsolete and will be removed in a future version. + /// The recommended alternative is to use Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions. + /// See https://go.microsoft.com/fwlink/?linkid=845470. + /// </summary> + [Obsolete("This is obsolete and will be removed in a future version. The recommended alternative is to use Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions. See https://go.microsoft.com/fwlink/?linkid=845470.")] + public override AuthenticationManager Authentication => _authenticationManager ?? (_authenticationManager = InitializeAuthenticationManager()); + + public override WebSocketManager WebSockets => _websockets ?? (_websockets = InitializeWebSocketManager()); + + + public override ClaimsPrincipal User + { + get + { + var user = HttpAuthenticationFeature.User; + if (user == null) + { + user = new ClaimsPrincipal(new ClaimsIdentity()); + HttpAuthenticationFeature.User = user; + } + return user; + } + set { HttpAuthenticationFeature.User = value; } + } + + public override IDictionary<object, object> Items + { + get { return ItemsFeature.Items; } + set { ItemsFeature.Items = value; } + } + + public override IServiceProvider RequestServices + { + get { return ServiceProvidersFeature.RequestServices; } + set { ServiceProvidersFeature.RequestServices = value; } + } + + public override CancellationToken RequestAborted + { + get { return LifetimeFeature.RequestAborted; } + set { LifetimeFeature.RequestAborted = value; } + } + + public override string TraceIdentifier + { + get { return RequestIdentifierFeature.TraceIdentifier; } + set { RequestIdentifierFeature.TraceIdentifier = value; } + } + + public override ISession Session + { + get + { + var feature = SessionFeatureOrNull; + if (feature == null) + { + throw new InvalidOperationException("Session has not been configured for this application " + + "or request."); + } + return feature.Session; + } + set + { + SessionFeature.Session = value; + } + } + + + + public override void Abort() + { + LifetimeFeature.Abort(); + } + + + protected virtual HttpRequest InitializeHttpRequest() => new DefaultHttpRequest(this); + protected virtual void UninitializeHttpRequest(HttpRequest instance) { } + + protected virtual HttpResponse InitializeHttpResponse() => new DefaultHttpResponse(this); + protected virtual void UninitializeHttpResponse(HttpResponse instance) { } + + protected virtual ConnectionInfo InitializeConnectionInfo() => new DefaultConnectionInfo(Features); + protected virtual void UninitializeConnectionInfo(ConnectionInfo instance) { } + + [Obsolete("This is obsolete and will be removed in a future version. See https://go.microsoft.com/fwlink/?linkid=845470.")] + protected virtual AuthenticationManager InitializeAuthenticationManager() => new DefaultAuthenticationManager(this); + [Obsolete("This is obsolete and will be removed in a future version. See https://go.microsoft.com/fwlink/?linkid=845470.")] + protected virtual void UninitializeAuthenticationManager(AuthenticationManager instance) { } + + protected virtual WebSocketManager InitializeWebSocketManager() => new DefaultWebSocketManager(Features); + protected virtual void UninitializeWebSocketManager(WebSocketManager instance) { } + + struct FeatureInterfaces + { + public IItemsFeature Items; + public IServiceProvidersFeature ServiceProviders; + public IHttpAuthenticationFeature Authentication; + public IHttpRequestLifetimeFeature Lifetime; + public ISessionFeature Session; + public IHttpRequestIdentifierFeature RequestIdentifier; + } + } +} diff --git a/src/Http/Http/src/Extensions/HttpRequestRewindExtensions.cs b/src/Http/Http/src/Extensions/HttpRequestRewindExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..557ee42155074a7e33c4a3f77067ebc43be40c9e --- /dev/null +++ b/src/Http/Http/src/Extensions/HttpRequestRewindExtensions.cs @@ -0,0 +1,91 @@ +// 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.Internal; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Extension methods for enabling buffering in an <see cref="HttpRequest"/>. + /// </summary> + public static class HttpRequestRewindExtensions + { + /// <summary> + /// Ensure the <paramref name="request"/> <see cref="HttpRequest.Body"/> can be read multiple times. Normally + /// buffers request bodies in memory; writes requests larger than 30K bytes to disk. + /// </summary> + /// <param name="request">The <see cref="HttpRequest"/> to prepare.</param> + /// <remarks> + /// Temporary files for larger requests are written to the location named in the <c>ASPNETCORE_TEMP</c> + /// environment variable, if any. If that environment variable is not defined, these files are written to the + /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. + /// </remarks> + public static void EnableBuffering(this HttpRequest request) + { + BufferingHelper.EnableRewind(request); + } + + /// <summary> + /// Ensure the <paramref name="request"/> <see cref="HttpRequest.Body"/> can be read multiple times. Normally + /// buffers request bodies in memory; writes requests larger than <paramref name="bufferThreshold"/> bytes to + /// disk. + /// </summary> + /// <param name="request">The <see cref="HttpRequest"/> to prepare.</param> + /// <param name="bufferThreshold"> + /// The maximum size in bytes of the in-memory <see cref="System.Buffers.ArrayPool{Byte}"/> used to buffer the + /// stream. Larger request bodies are written to disk. + /// </param> + /// <remarks> + /// Temporary files for larger requests are written to the location named in the <c>ASPNETCORE_TEMP</c> + /// environment variable, if any. If that environment variable is not defined, these files are written to the + /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. + /// </remarks> + public static void EnableBuffering(this HttpRequest request, int bufferThreshold) + { + BufferingHelper.EnableRewind(request, bufferThreshold); + } + + /// <summary> + /// Ensure the <paramref name="request"/> <see cref="HttpRequest.Body"/> can be read multiple times. Normally + /// buffers request bodies in memory; writes requests larger than 30K bytes to disk. + /// </summary> + /// <param name="request">The <see cref="HttpRequest"/> to prepare.</param> + /// <param name="bufferLimit"> + /// The maximum size in bytes of the request body. An attempt to read beyond this limit will cause an + /// <see cref="System.IO.IOException"/>. + /// </param> + /// <remarks> + /// Temporary files for larger requests are written to the location named in the <c>ASPNETCORE_TEMP</c> + /// environment variable, if any. If that environment variable is not defined, these files are written to the + /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. + /// </remarks> + public static void EnableBuffering(this HttpRequest request, long bufferLimit) + { + BufferingHelper.EnableRewind(request, bufferLimit: bufferLimit); + } + + /// <summary> + /// Ensure the <paramref name="request"/> <see cref="HttpRequest.Body"/> can be read multiple times. Normally + /// buffers request bodies in memory; writes requests larger than <paramref name="bufferThreshold"/> bytes to + /// disk. + /// </summary> + /// <param name="request">The <see cref="HttpRequest"/> to prepare.</param> + /// <param name="bufferThreshold"> + /// The maximum size in bytes of the in-memory <see cref="System.Buffers.ArrayPool{Byte}"/> used to buffer the + /// stream. Larger request bodies are written to disk. + /// </param> + /// <param name="bufferLimit"> + /// The maximum size in bytes of the request body. An attempt to read beyond this limit will cause an + /// <see cref="System.IO.IOException"/>. + /// </param> + /// <remarks> + /// Temporary files for larger requests are written to the location named in the <c>ASPNETCORE_TEMP</c> + /// environment variable, if any. If that environment variable is not defined, these files are written to the + /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. + /// </remarks> + public static void EnableBuffering(this HttpRequest request, int bufferThreshold, long bufferLimit) + { + BufferingHelper.EnableRewind(request, bufferThreshold, bufferLimit); + } + } +} diff --git a/src/Http/Http/src/Features/Authentication/HttpAuthenticationFeature.cs b/src/Http/Http/src/Features/Authentication/HttpAuthenticationFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..9a14b657121ba467bfd693d7ff801f05269e9b84 --- /dev/null +++ b/src/Http/Http/src/Features/Authentication/HttpAuthenticationFeature.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public class HttpAuthenticationFeature : IHttpAuthenticationFeature + { + public ClaimsPrincipal User + { + get; + set; + } + + public IAuthenticationHandler Handler + { + get; + set; + } + } +} diff --git a/src/Http/Http/src/Features/DefaultSessionFeature.cs b/src/Http/Http/src/Features/DefaultSessionFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..6790133467c8c59ee6eb16c64a319f9bf561ba03 --- /dev/null +++ b/src/Http/Http/src/Features/DefaultSessionFeature.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features +{ + /// <summary> + /// This type exists only for the purpose of unit testing where the user can directly set the + /// <see cref="HttpContext.Session"/> property without the need for creating a <see cref="ISessionFeature"/>. + /// </summary> + public class DefaultSessionFeature : ISessionFeature + { + public ISession Session { get; set; } + } +} diff --git a/src/Http/Http/src/Features/FormFeature.cs b/src/Http/Http/src/Features/FormFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..f091e3b16647e4f638cb9b8085a53aef508b682b --- /dev/null +++ b/src/Http/Http/src/Features/FormFeature.cs @@ -0,0 +1,323 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class FormFeature : IFormFeature + { + private static readonly FormOptions DefaultFormOptions = new FormOptions(); + + private readonly HttpRequest _request; + private readonly FormOptions _options; + private Task<IFormCollection> _parsedFormTask; + private IFormCollection _form; + + public FormFeature(IFormCollection form) + { + if (form == null) + { + throw new ArgumentNullException(nameof(form)); + } + + Form = form; + } + public FormFeature(HttpRequest request) + : this(request, DefaultFormOptions) + { + } + + public FormFeature(HttpRequest request, FormOptions options) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _request = request; + _options = options; + } + + private MediaTypeHeaderValue ContentType + { + get + { + MediaTypeHeaderValue mt; + MediaTypeHeaderValue.TryParse(_request.ContentType, out mt); + return mt; + } + } + + public bool HasFormContentType + { + get + { + // Set directly + if (Form != null) + { + return true; + } + + var contentType = ContentType; + return HasApplicationFormContentType(contentType) || HasMultipartFormContentType(contentType); + } + } + + public IFormCollection Form + { + get { return _form; } + set + { + _parsedFormTask = null; + _form = value; + } + } + + public IFormCollection ReadForm() + { + if (Form != null) + { + return Form; + } + + if (!HasFormContentType) + { + throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType); + } + + // TODO: Issue #456 Avoid Sync-over-Async http://blogs.msdn.com/b/pfxteam/archive/2012/04/13/10293638.aspx + // TODO: How do we prevent thread exhaustion? + return ReadFormAsync().GetAwaiter().GetResult(); + } + + public Task<IFormCollection> ReadFormAsync() => ReadFormAsync(CancellationToken.None); + + public Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken) + { + // Avoid state machine and task allocation for repeated reads + if (_parsedFormTask == null) + { + if (Form != null) + { + _parsedFormTask = Task.FromResult(Form); + } + else + { + _parsedFormTask = InnerReadFormAsync(cancellationToken); + } + } + return _parsedFormTask; + } + + private async Task<IFormCollection> InnerReadFormAsync(CancellationToken cancellationToken) + { + if (!HasFormContentType) + { + throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType); + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (_options.BufferBody) + { + _request.EnableRewind(_options.MemoryBufferThreshold, _options.BufferBodyLengthLimit); + } + + FormCollection formFields = null; + FormFileCollection files = null; + + // Some of these code paths use StreamReader which does not support cancellation tokens. + using (cancellationToken.Register((state) => ((HttpContext)state).Abort(), _request.HttpContext)) + { + var contentType = ContentType; + // Check the content-type + if (HasApplicationFormContentType(contentType)) + { + var encoding = FilterEncoding(contentType.Encoding); + using (var formReader = new FormReader(_request.Body, encoding) + { + ValueCountLimit = _options.ValueCountLimit, + KeyLengthLimit = _options.KeyLengthLimit, + ValueLengthLimit = _options.ValueLengthLimit, + }) + { + formFields = new FormCollection(await formReader.ReadFormAsync(cancellationToken)); + } + } + else if (HasMultipartFormContentType(contentType)) + { + var formAccumulator = new KeyValueAccumulator(); + + var boundary = GetBoundary(contentType, _options.MultipartBoundaryLengthLimit); + var multipartReader = new MultipartReader(boundary, _request.Body) + { + HeadersCountLimit = _options.MultipartHeadersCountLimit, + HeadersLengthLimit = _options.MultipartHeadersLengthLimit, + BodyLengthLimit = _options.MultipartBodyLengthLimit, + }; + var section = await multipartReader.ReadNextSectionAsync(cancellationToken); + while (section != null) + { + // Parse the content disposition here and pass it further to avoid reparsings + ContentDispositionHeaderValue contentDisposition; + ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition); + + if (contentDisposition.IsFileDisposition()) + { + var fileSection = new FileMultipartSection(section, contentDisposition); + + // Enable buffering for the file if not already done for the full body + section.EnableRewind( + _request.HttpContext.Response.RegisterForDispose, + _options.MemoryBufferThreshold, _options.MultipartBodyLengthLimit); + + // Find the end + await section.Body.DrainAsync(cancellationToken); + + var name = fileSection.Name; + var fileName = fileSection.FileName; + + FormFile file; + if (section.BaseStreamOffset.HasValue) + { + // Relative reference to buffered request body + file = new FormFile(_request.Body, section.BaseStreamOffset.Value, section.Body.Length, name, fileName); + } + else + { + // Individually buffered file body + file = new FormFile(section.Body, 0, section.Body.Length, name, fileName); + } + file.Headers = new HeaderDictionary(section.Headers); + + if (files == null) + { + files = new FormFileCollection(); + } + if (files.Count >= _options.ValueCountLimit) + { + throw new InvalidDataException($"Form value count limit {_options.ValueCountLimit} exceeded."); + } + files.Add(file); + } + else if (contentDisposition.IsFormDisposition()) + { + var formDataSection = new FormMultipartSection(section, contentDisposition); + + // Content-Disposition: form-data; name="key" + // + // value + + // Do not limit the key name length here because the mulipart headers length limit is already in effect. + var key = formDataSection.Name; + var value = await formDataSection.GetValueAsync(); + + formAccumulator.Append(key, value); + if (formAccumulator.ValueCount > _options.ValueCountLimit) + { + throw new InvalidDataException($"Form value count limit {_options.ValueCountLimit} exceeded."); + } + } + else + { + System.Diagnostics.Debug.Assert(false, "Unrecognized content-disposition for this section: " + section.ContentDisposition); + } + + section = await multipartReader.ReadNextSectionAsync(cancellationToken); + } + + if (formAccumulator.HasValues) + { + formFields = new FormCollection(formAccumulator.GetResults(), files); + } + } + } + + // Rewind so later readers don't have to. + if (_request.Body.CanSeek) + { + _request.Body.Seek(0, SeekOrigin.Begin); + } + + if (formFields != null) + { + Form = formFields; + } + else if (files != null) + { + Form = new FormCollection(null, files); + } + else + { + Form = FormCollection.Empty; + } + + return Form; + } + + private Encoding FilterEncoding(Encoding encoding) + { + // UTF-7 is insecure and should not be honored. UTF-8 will succeed for most cases. + if (encoding == null || Encoding.UTF7.Equals(encoding)) + { + return Encoding.UTF8; + } + return encoding; + } + + private bool HasApplicationFormContentType(MediaTypeHeaderValue contentType) + { + // Content-Type: application/x-www-form-urlencoded; charset=utf-8 + return contentType != null && contentType.MediaType.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase); + } + + private bool HasMultipartFormContentType(MediaTypeHeaderValue contentType) + { + // Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq + return contentType != null && contentType.MediaType.Equals("multipart/form-data", StringComparison.OrdinalIgnoreCase); + } + + private bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition) + { + // Content-Disposition: form-data; name="key"; + return contentDisposition != null && contentDisposition.DispositionType.Equals("form-data") + && StringSegment.IsNullOrEmpty(contentDisposition.FileName) && StringSegment.IsNullOrEmpty(contentDisposition.FileNameStar); + } + + private bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition) + { + // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg" + return contentDisposition != null && contentDisposition.DispositionType.Equals("form-data") + && (!StringSegment.IsNullOrEmpty(contentDisposition.FileName) || !StringSegment.IsNullOrEmpty(contentDisposition.FileNameStar)); + } + + // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq" + // The spec says 70 characters is a reasonable limit. + private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit) + { + var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary); + if (StringSegment.IsNullOrEmpty(boundary)) + { + throw new InvalidDataException("Missing content-type boundary."); + } + if (boundary.Length > lengthLimit) + { + throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded."); + } + return boundary.ToString(); + } + } +} diff --git a/src/Http/Http/src/Features/FormOptions.cs b/src/Http/Http/src/Features/FormOptions.cs new file mode 100644 index 0000000000000000000000000000000000000000..17e521b215748e24d64aff22c66e32dac8ce48ed --- /dev/null +++ b/src/Http/Http/src/Features/FormOptions.cs @@ -0,0 +1,78 @@ +// 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.IO; +using Microsoft.AspNetCore.WebUtilities; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class FormOptions + { + public const int DefaultMemoryBufferThreshold = 1024 * 64; + public const int DefaultBufferBodyLengthLimit = 1024 * 1024 * 128; + public const int DefaultMultipartBoundaryLengthLimit = 128; + public const long DefaultMultipartBodyLengthLimit = 1024 * 1024 * 128; + + /// <summary> + /// Enables full request body buffering. Use this if multiple components need to read the raw stream. + /// The default value is false. + /// </summary> + public bool BufferBody { get; set; } = false; + + /// <summary> + /// If <see cref="BufferBody"/> is enabled, this many bytes of the body will be buffered in memory. + /// If this threshold is exceeded then the buffer will be moved to a temp file on disk instead. + /// This also applies when buffering individual multipart section bodies. + /// </summary> + public int MemoryBufferThreshold { get; set; } = DefaultMemoryBufferThreshold; + + /// <summary> + /// If <see cref="BufferBody"/> is enabled, this is the limit for the total number of bytes that will + /// be buffered. Forms that exceed this limit will throw an <see cref="InvalidDataException"/> when parsed. + /// </summary> + public long BufferBodyLengthLimit { get; set; } = DefaultBufferBodyLengthLimit; + + /// <summary> + /// A limit for the number of form entries to allow. + /// Forms that exceed this limit will throw an <see cref="InvalidDataException"/> when parsed. + /// </summary> + public int ValueCountLimit { get; set; } = FormReader.DefaultValueCountLimit; + + /// <summary> + /// A limit on the length of individual keys. Forms containing keys that exceed this limit will + /// throw an <see cref="InvalidDataException"/> when parsed. + /// </summary> + public int KeyLengthLimit { get; set; } = FormReader.DefaultKeyLengthLimit; + + /// <summary> + /// A limit on the length of individual form values. Forms containing values that exceed this + /// limit will throw an <see cref="InvalidDataException"/> when parsed. + /// </summary> + public int ValueLengthLimit { get; set; } = FormReader.DefaultValueLengthLimit; + + /// <summary> + /// A limit for the length of the boundary identifier. Forms with boundaries that exceed this + /// limit will throw an <see cref="InvalidDataException"/> when parsed. + /// </summary> + public int MultipartBoundaryLengthLimit { get; set; } = DefaultMultipartBoundaryLengthLimit; + + /// <summary> + /// A limit for the number of headers to allow in each multipart section. Headers with the same name will + /// be combined. Form sections that exceed this limit will throw an <see cref="InvalidDataException"/> + /// when parsed. + /// </summary> + public int MultipartHeadersCountLimit { get; set; } = MultipartReader.DefaultHeadersCountLimit; + + /// <summary> + /// A limit for the total length of the header keys and values in each multipart section. + /// Form sections that exceed this limit will throw an <see cref="InvalidDataException"/> when parsed. + /// </summary> + public int MultipartHeadersLengthLimit { get; set; } = MultipartReader.DefaultHeadersLengthLimit; + + /// <summary> + /// A limit for the length of each multipart body. Forms sections that exceed this limit will throw an + /// <see cref="InvalidDataException"/> when parsed. + /// </summary> + public long MultipartBodyLengthLimit { get; set; } = DefaultMultipartBodyLengthLimit; + } +} diff --git a/src/Http/Http/src/Features/HttpConnectionFeature.cs b/src/Http/Http/src/Features/HttpConnectionFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..2e8d5b0a1c6db761b27e69b9ad2c5a49bc6d3f60 --- /dev/null +++ b/src/Http/Http/src/Features/HttpConnectionFeature.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class HttpConnectionFeature : IHttpConnectionFeature + { + public string ConnectionId { get; set; } + + public IPAddress LocalIpAddress { get; set; } + + public int LocalPort { get; set; } + + public IPAddress RemoteIpAddress { get; set; } + + public int RemotePort { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/HttpRequestFeature.cs b/src/Http/Http/src/Features/HttpRequestFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..b8b667bf4e43e7a644d3117f30309e876699df81 --- /dev/null +++ b/src/Http/Http/src/Features/HttpRequestFeature.cs @@ -0,0 +1,33 @@ +// 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.IO; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class HttpRequestFeature : IHttpRequestFeature + { + public HttpRequestFeature() + { + Headers = new HeaderDictionary(); + Body = Stream.Null; + Protocol = string.Empty; + Scheme = string.Empty; + Method = string.Empty; + PathBase = string.Empty; + Path = string.Empty; + QueryString = string.Empty; + RawTarget = string.Empty; + } + + public string Protocol { get; set; } + public string Scheme { get; set; } + public string Method { get; set; } + public string PathBase { get; set; } + public string Path { get; set; } + public string QueryString { get; set; } + public string RawTarget { get; set; } + public IHeaderDictionary Headers { get; set; } + public Stream Body { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/HttpRequestIdentifierFeature.cs b/src/Http/Http/src/Features/HttpRequestIdentifierFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..34663937a5a5ba09b9d0c661d043d58382dc4c7a --- /dev/null +++ b/src/Http/Http/src/Features/HttpRequestIdentifierFeature.cs @@ -0,0 +1,64 @@ +// 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; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class HttpRequestIdentifierFeature : IHttpRequestIdentifierFeature + { + // Base32 encoding - in ascii sort order for easy text based sorting + private static readonly string _encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV"; + // Seed the _requestId for this application instance with + // the number of 100-nanosecond intervals that have elapsed since 12:00:00 midnight, January 1, 0001 + // for a roughly increasing _requestId over restarts + private static long _requestId = DateTime.UtcNow.Ticks; + + private string _id = null; + + public string TraceIdentifier + { + get + { + // Don't incur the cost of generating the request ID until it's asked for + if (_id == null) + { + _id = GenerateRequestId(Interlocked.Increment(ref _requestId)); + } + return _id; + } + set + { + _id = value; + } + } + + private static unsafe string GenerateRequestId(long id) + { + // The following routine is ~310% faster than calling long.ToString() on x64 + // and ~600% faster than calling long.ToString() on x86 in tight loops of 1 million+ iterations + // See: https://github.com/aspnet/Hosting/pull/385 + + // stackalloc to allocate array on stack rather than heap + char* charBuffer = stackalloc char[13]; + + charBuffer[0] = _encode32Chars[(int)(id >> 60) & 31]; + charBuffer[1] = _encode32Chars[(int)(id >> 55) & 31]; + charBuffer[2] = _encode32Chars[(int)(id >> 50) & 31]; + charBuffer[3] = _encode32Chars[(int)(id >> 45) & 31]; + charBuffer[4] = _encode32Chars[(int)(id >> 40) & 31]; + charBuffer[5] = _encode32Chars[(int)(id >> 35) & 31]; + charBuffer[6] = _encode32Chars[(int)(id >> 30) & 31]; + charBuffer[7] = _encode32Chars[(int)(id >> 25) & 31]; + charBuffer[8] = _encode32Chars[(int)(id >> 20) & 31]; + charBuffer[9] = _encode32Chars[(int)(id >> 15) & 31]; + charBuffer[10] = _encode32Chars[(int)(id >> 10) & 31]; + charBuffer[11] = _encode32Chars[(int)(id >> 5) & 31]; + charBuffer[12] = _encode32Chars[(int)id & 31]; + + // string ctor overload that takes char* + return new string(charBuffer, 0, 13); + } + } +} diff --git a/src/Http/Http/src/Features/HttpRequestLifetimeFeature.cs b/src/Http/Http/src/Features/HttpRequestLifetimeFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..df327d07582112c8d2fc2aa791f47b8863746d35 --- /dev/null +++ b/src/Http/Http/src/Features/HttpRequestLifetimeFeature.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class HttpRequestLifetimeFeature : IHttpRequestLifetimeFeature + { + public CancellationToken RequestAborted { get; set; } + + public void Abort() + { + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/HttpResponseFeature.cs b/src/Http/Http/src/Features/HttpResponseFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..a02a79088bcd5831e96788a7d53d222bf112e764 --- /dev/null +++ b/src/Http/Http/src/Features/HttpResponseFeature.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class HttpResponseFeature : IHttpResponseFeature + { + public HttpResponseFeature() + { + StatusCode = 200; + Headers = new HeaderDictionary(); + Body = Stream.Null; + } + + public int StatusCode { get; set; } + + public string ReasonPhrase { get; set; } + + public IHeaderDictionary Headers { get; set; } + + public Stream Body { get; set; } + + public virtual bool HasStarted + { + get { return false; } + } + + public virtual void OnStarting(Func<object, Task> callback, object state) + { + } + + public virtual void OnCompleted(Func<object, Task> callback, object state) + { + } + } +} diff --git a/src/Http/Http/src/Features/ItemsFeature.cs b/src/Http/Http/src/Features/ItemsFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..6bf0669b45c121857781f57815c0eae48cd82315 --- /dev/null +++ b/src/Http/Http/src/Features/ItemsFeature.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Http.Internal; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class ItemsFeature : IItemsFeature + { + public ItemsFeature() + { + Items = new ItemsDictionary(); + } + + public IDictionary<object, object> Items { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/QueryFeature.cs b/src/Http/Http/src/Features/QueryFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..36781ef16eba8834d2302dc60272cb1db68743f9 --- /dev/null +++ b/src/Http/Http/src/Features/QueryFeature.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.AspNetCore.WebUtilities; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class QueryFeature : IQueryFeature + { + // Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func<IFeatureCollection, IHttpRequestFeature> _nullRequestFeature = f => null; + + private FeatureReferences<IHttpRequestFeature> _features; + + private string _original; + private IQueryCollection _parsedValues; + + public QueryFeature(IQueryCollection query) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + _parsedValues = query; + } + + public QueryFeature(IFeatureCollection features) + { + if (features == null) + { + throw new ArgumentNullException(nameof(features)); + } + + _features = new FeatureReferences<IHttpRequestFeature>(features); + } + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache, _nullRequestFeature); + + public IQueryCollection Query + { + get + { + if (_features.Collection == null) + { + if (_parsedValues == null) + { + _parsedValues = QueryCollection.Empty; + } + return _parsedValues; + } + + var current = HttpRequestFeature.QueryString; + if (_parsedValues == null || !string.Equals(_original, current, StringComparison.Ordinal)) + { + _original = current; + + var result = QueryHelpers.ParseNullableQuery(current); + + if (result == null) + { + _parsedValues = QueryCollection.Empty; + } + else + { + _parsedValues = new QueryCollection(result); + } + } + return _parsedValues; + } + set + { + _parsedValues = value; + if (_features.Collection != null) + { + if (value == null) + { + _original = string.Empty; + HttpRequestFeature.QueryString = string.Empty; + } + else + { + _original = QueryString.Create(_parsedValues).ToString(); + HttpRequestFeature.QueryString = _original; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/RequestCookiesFeature.cs b/src/Http/Http/src/Features/RequestCookiesFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..cd37b360a4efcad046600a0d22f99f0a269a0340 --- /dev/null +++ b/src/Http/Http/src/Features/RequestCookiesFeature.cs @@ -0,0 +1,96 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class RequestCookiesFeature : IRequestCookiesFeature + { + // Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func<IFeatureCollection, IHttpRequestFeature> _nullRequestFeature = f => null; + + private FeatureReferences<IHttpRequestFeature> _features; + private StringValues _original; + private IRequestCookieCollection _parsedValues; + + public RequestCookiesFeature(IRequestCookieCollection cookies) + { + if (cookies == null) + { + throw new ArgumentNullException(nameof(cookies)); + } + + _parsedValues = cookies; + } + + public RequestCookiesFeature(IFeatureCollection features) + { + if (features == null) + { + throw new ArgumentNullException(nameof(features)); + } + + _features = new FeatureReferences<IHttpRequestFeature>(features); + } + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache, _nullRequestFeature); + + public IRequestCookieCollection Cookies + { + get + { + if (_features.Collection == null) + { + if (_parsedValues == null) + { + _parsedValues = RequestCookieCollection.Empty; + } + return _parsedValues; + } + + var headers = HttpRequestFeature.Headers; + StringValues current; + if (!headers.TryGetValue(HeaderNames.Cookie, out current)) + { + current = string.Empty; + } + + if (_parsedValues == null || _original != current) + { + _original = current; + _parsedValues = RequestCookieCollection.Parse(current.ToArray()); + } + + return _parsedValues; + } + set + { + _parsedValues = value; + _original = StringValues.Empty; + if (_features.Collection != null) + { + if (_parsedValues == null || _parsedValues.Count == 0) + { + HttpRequestFeature.Headers.Remove(HeaderNames.Cookie); + } + else + { + var headers = new List<string>(); + foreach (var pair in _parsedValues) + { + headers.Add(new CookieHeaderValue(pair.Key, pair.Value).ToString()); + } + _original = headers.ToArray(); + HttpRequestFeature.Headers[HeaderNames.Cookie] = _original; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/ResponseCookiesFeature.cs b/src/Http/Http/src/Features/ResponseCookiesFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..0d9444b0f5626559aa64a1de54f3d4b9e3e399cd --- /dev/null +++ b/src/Http/Http/src/Features/ResponseCookiesFeature.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Text; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// <summary> + /// Default implementation of <see cref="IResponseCookiesFeature"/>. + /// </summary> + public class ResponseCookiesFeature : IResponseCookiesFeature + { + // Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func<IFeatureCollection, IHttpResponseFeature> _nullResponseFeature = f => null; + + private FeatureReferences<IHttpResponseFeature> _features; + private IResponseCookies _cookiesCollection; + + /// <summary> + /// Initializes a new <see cref="ResponseCookiesFeature"/> instance. + /// </summary> + /// <param name="features"> + /// <see cref="IFeatureCollection"/> containing all defined features, including this + /// <see cref="IResponseCookiesFeature"/> and the <see cref="IHttpResponseFeature"/>. + /// </param> + public ResponseCookiesFeature(IFeatureCollection features) + : this(features, builderPool: null) + { + } + + /// <summary> + /// Initializes a new <see cref="ResponseCookiesFeature"/> instance. + /// </summary> + /// <param name="features"> + /// <see cref="IFeatureCollection"/> containing all defined features, including this + /// <see cref="IResponseCookiesFeature"/> and the <see cref="IHttpResponseFeature"/>. + /// </param> + /// <param name="builderPool">The <see cref="ObjectPool{T}"/>, if available.</param> + public ResponseCookiesFeature(IFeatureCollection features, ObjectPool<StringBuilder> builderPool) + { + if (features == null) + { + throw new ArgumentNullException(nameof(features)); + } + + _features = new FeatureReferences<IHttpResponseFeature>(features); + } + + private IHttpResponseFeature HttpResponseFeature => _features.Fetch(ref _features.Cache, _nullResponseFeature); + + /// <inheritdoc /> + public IResponseCookies Cookies + { + get + { + if (_cookiesCollection == null) + { + var headers = HttpResponseFeature.Headers; + _cookiesCollection = new ResponseCookies(headers, null); + } + + return _cookiesCollection; + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/ServiceProvidersFeature.cs b/src/Http/Http/src/Features/ServiceProvidersFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..d1cf4e6cba83a2eef3e48292d2564e55f13c26d9 --- /dev/null +++ b/src/Http/Http/src/Features/ServiceProvidersFeature.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class ServiceProvidersFeature : IServiceProvidersFeature + { + public IServiceProvider RequestServices { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/TlsConnectionFeature.cs b/src/Http/Http/src/Features/TlsConnectionFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..f9bfcdef7feebf5fc28925979708f307cf7363ab --- /dev/null +++ b/src/Http/Http/src/Features/TlsConnectionFeature.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class TlsConnectionFeature : ITlsConnectionFeature + { + public X509Certificate2 ClientCertificate { get; set; } + + public Task<X509Certificate2> GetClientCertificateAsync(CancellationToken cancellationToken) + { + return Task.FromResult(ClientCertificate); + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/FormCollection.cs b/src/Http/Http/src/FormCollection.cs new file mode 100644 index 0000000000000000000000000000000000000000..23709b2bb064ed74634bad14bfb78cdf3ef721b4 --- /dev/null +++ b/src/Http/Http/src/FormCollection.cs @@ -0,0 +1,228 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Contains the parsed form values. + /// </summary> + public class FormCollection : IFormCollection + { + public static readonly FormCollection Empty = new FormCollection(); + private static readonly string[] EmptyKeys = Array.Empty<string>(); + private static readonly StringValues[] EmptyValues = Array.Empty<StringValues>(); + private static readonly Enumerator EmptyEnumerator = new Enumerator(); + // Pre-box + private static readonly IEnumerator<KeyValuePair<string, StringValues>> EmptyIEnumeratorType = EmptyEnumerator; + private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; + + private static IFormFileCollection EmptyFiles = new FormFileCollection(); + + private IFormFileCollection _files; + + private FormCollection() + { + // For static Empty + } + + public FormCollection(Dictionary<string, StringValues> fields, IFormFileCollection files = null) + { + // can be null + Store = fields; + _files = files; + } + + public IFormFileCollection Files + { + get + { + return _files ?? EmptyFiles; + } + private set { _files = value; } + } + + private Dictionary<string, StringValues> Store { get; set; } + + /// <summary> + /// Get or sets the associated value from the collection as a single string. + /// </summary> + /// <param name="key">The header name.</param> + /// <returns>the associated value from the collection as a StringValues or StringValues.Empty if the key is not present.</returns> + public StringValues this[string key] + { + get + { + if (Store == null) + { + return StringValues.Empty; + } + + StringValues value; + if (TryGetValue(key, out value)) + { + return value; + } + return StringValues.Empty; + } + } + + /// <summary> + /// Gets the number of elements contained in the <see cref="HeaderDictionary" />;. + /// </summary> + /// <returns>The number of elements contained in the <see cref="HeaderDictionary" />.</returns> + public int Count + { + get + { + return Store?.Count ?? 0; + } + } + + public ICollection<string> Keys + { + get + { + if (Store == null) + { + return EmptyKeys; + } + return Store.Keys; + } + } + + /// <summary> + /// Determines whether the <see cref="HeaderDictionary" /> contains a specific key. + /// </summary> + /// <param name="key">The key.</param> + /// <returns>true if the <see cref="HeaderDictionary" /> contains a specific key; otherwise, false.</returns> + public bool ContainsKey(string key) + { + if (Store == null) + { + return false; + } + return Store.ContainsKey(key); + } + + /// <summary> + /// Retrieves a value from the dictionary. + /// </summary> + /// <param name="key">The header name.</param> + /// <param name="value">The value.</param> + /// <returns>true if the <see cref="HeaderDictionary" /> contains the key; otherwise, false.</returns> + public bool TryGetValue(string key, out StringValues value) + { + if (Store == null) + { + value = default(StringValues); + return false; + } + return Store.TryGetValue(key, out value); + } + + /// <summary> + /// Returns an struct enumerator that iterates through a collection without boxing and is also used via the <see cref="IFormCollection" /> interface. + /// </summary> + /// <returns>An <see cref="Enumerator" /> object that can be used to iterate through the collection.</returns> + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyEnumerator; + } + // Non-boxed Enumerator + return new Enumerator(Store.GetEnumerator()); + } + + /// <summary> + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// </summary> + /// <returns>An <see cref="IEnumerator" /> object that can be used to iterate through the collection.</returns> + IEnumerator<KeyValuePair<string, StringValues>> IEnumerable<KeyValuePair<string, StringValues>>.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumeratorType; + } + // Boxed Enumerator + return Store.GetEnumerator(); + } + + /// <summary> + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// </summary> + /// <returns>An <see cref="IEnumerator" /> object that can be used to iterate through the collection.</returns> + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + // Boxed Enumerator + return Store.GetEnumerator(); + } + + public struct Enumerator : IEnumerator<KeyValuePair<string, StringValues>> + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary<string, StringValues>.Enumerator _dictionaryEnumerator; + private bool _notEmpty; + + internal Enumerator(Dictionary<string, StringValues>.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } + + public bool MoveNext() + { + if (_notEmpty) + { + return _dictionaryEnumerator.MoveNext(); + } + return false; + } + + public KeyValuePair<string, StringValues> Current + { + get + { + if (_notEmpty) + { + return _dictionaryEnumerator.Current; + } + return default(KeyValuePair<string, StringValues>); + } + } + + public void Dispose() + { + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + void IEnumerator.Reset() + { + if (_notEmpty) + { + ((IEnumerator)_dictionaryEnumerator).Reset(); + } + } + } + } +} diff --git a/src/Http/Http/src/HeaderDictionary.cs b/src/Http/Http/src/HeaderDictionary.cs new file mode 100644 index 0000000000000000000000000000000000000000..bc0b7a26ce32baf175897d0cdbeaf847858782ee --- /dev/null +++ b/src/Http/Http/src/HeaderDictionary.cs @@ -0,0 +1,416 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Represents a wrapper for RequestHeaders and ResponseHeaders. + /// </summary> + public class HeaderDictionary : IHeaderDictionary + { + private static readonly string[] EmptyKeys = Array.Empty<string>(); + private static readonly StringValues[] EmptyValues = Array.Empty<StringValues>(); + private static readonly Enumerator EmptyEnumerator = new Enumerator(); + // Pre-box + private static readonly IEnumerator<KeyValuePair<string, StringValues>> EmptyIEnumeratorType = EmptyEnumerator; + private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; + + public HeaderDictionary() + { + } + + public HeaderDictionary(Dictionary<string, StringValues> store) + { + Store = store; + } + + public HeaderDictionary(int capacity) + { + EnsureStore(capacity); + } + + private Dictionary<string, StringValues> Store { get; set; } + + private void EnsureStore(int capacity) + { + if (Store == null) + { + Store = new Dictionary<string, StringValues>(capacity, StringComparer.OrdinalIgnoreCase); + } + } + + /// <summary> + /// Get or sets the associated value from the collection as a single string. + /// </summary> + /// <param name="key">The header name.</param> + /// <returns>the associated value from the collection as a StringValues or StringValues.Empty if the key is not present.</returns> + public StringValues this[string key] + { + get + { + if (Store == null) + { + return StringValues.Empty; + } + + StringValues value; + if (TryGetValue(key, out value)) + { + return value; + } + return StringValues.Empty; + } + set + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + ThrowIfReadOnly(); + + if (StringValues.IsNullOrEmpty(value)) + { + Store?.Remove(key); + } + else + { + EnsureStore(1); + Store[key] = value; + } + } + } + + /// <summary> + /// Throws KeyNotFoundException if the key is not present. + /// </summary> + /// <param name="key">The header name.</param> + /// <returns></returns> + StringValues IDictionary<string, StringValues>.this[string key] + { + get { return Store[key]; } + set + { + ThrowIfReadOnly(); + this[key] = value; + } + } + + public long? ContentLength + { + get + { + long value; + var rawValue = this[HeaderNames.ContentLength]; + if (rawValue.Count == 1 && + !string.IsNullOrEmpty(rawValue[0]) && + HeaderUtilities.TryParseNonNegativeInt64(new StringSegment(rawValue[0]).Trim(), out value)) + { + return value; + } + + return null; + } + set + { + ThrowIfReadOnly(); + if (value.HasValue) + { + this[HeaderNames.ContentLength] = HeaderUtilities.FormatNonNegativeInt64(value.Value); + } + else + { + this.Remove(HeaderNames.ContentLength); + } + } + } + + /// <summary> + /// Gets the number of elements contained in the <see cref="HeaderDictionary" />;. + /// </summary> + /// <returns>The number of elements contained in the <see cref="HeaderDictionary" />.</returns> + public int Count => Store?.Count ?? 0; + + /// <summary> + /// Gets a value that indicates whether the <see cref="HeaderDictionary" /> is in read-only mode. + /// </summary> + /// <returns>true if the <see cref="HeaderDictionary" /> is in read-only mode; otherwise, false.</returns> + public bool IsReadOnly { get; set; } + + public ICollection<string> Keys + { + get + { + if (Store == null) + { + return EmptyKeys; + } + return Store.Keys; + } + } + + public ICollection<StringValues> Values + { + get + { + if (Store == null) + { + return EmptyValues; + } + return Store.Values; + } + } + + /// <summary> + /// Adds a new list of items to the collection. + /// </summary> + /// <param name="item">The item to add.</param> + public void Add(KeyValuePair<string, StringValues> item) + { + if (item.Key == null) + { + throw new ArgumentNullException("The key is null"); + } + ThrowIfReadOnly(); + EnsureStore(1); + Store.Add(item.Key, item.Value); + } + + /// <summary> + /// Adds the given header and values to the collection. + /// </summary> + /// <param name="key">The header name.</param> + /// <param name="value">The header values.</param> + public void Add(string key, StringValues value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + ThrowIfReadOnly(); + EnsureStore(1); + Store.Add(key, value); + } + + /// <summary> + /// Clears the entire list of objects. + /// </summary> + public void Clear() + { + ThrowIfReadOnly(); + Store?.Clear(); + } + + /// <summary> + /// Returns a value indicating whether the specified object occurs within this collection. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>true if the specified object occurs within this collection; otherwise, false.</returns> + public bool Contains(KeyValuePair<string, StringValues> item) + { + StringValues value; + if (Store == null || + !Store.TryGetValue(item.Key, out value) || + !StringValues.Equals(value, item.Value)) + { + return false; + } + return true; + } + + /// <summary> + /// Determines whether the <see cref="HeaderDictionary" /> contains a specific key. + /// </summary> + /// <param name="key">The key.</param> + /// <returns>true if the <see cref="HeaderDictionary" /> contains a specific key; otherwise, false.</returns> + public bool ContainsKey(string key) + { + if (Store == null) + { + return false; + } + return Store.ContainsKey(key); + } + + /// <summary> + /// Copies the <see cref="HeaderDictionary" /> elements to a one-dimensional Array instance at the specified index. + /// </summary> + /// <param name="array">The one-dimensional Array that is the destination of the specified objects copied from the <see cref="HeaderDictionary" />.</param> + /// <param name="arrayIndex">The zero-based index in <paramref name="array" /> at which copying begins.</param> + public void CopyTo(KeyValuePair<string, StringValues>[] array, int arrayIndex) + { + if (Store == null) + { + return; + } + + foreach (var item in Store) + { + array[arrayIndex] = item; + arrayIndex++; + } + } + + /// <summary> + /// Removes the given item from the the collection. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>true if the specified object was removed from the collection; otherwise, false.</returns> + public bool Remove(KeyValuePair<string, StringValues> item) + { + ThrowIfReadOnly(); + if (Store == null) + { + return false; + } + + StringValues value; + + if (Store.TryGetValue(item.Key, out value) && StringValues.Equals(item.Value, value)) + { + return Store.Remove(item.Key); + } + return false; + } + + /// <summary> + /// Removes the given header from the collection. + /// </summary> + /// <param name="key">The header name.</param> + /// <returns>true if the specified object was removed from the collection; otherwise, false.</returns> + public bool Remove(string key) + { + ThrowIfReadOnly(); + if (Store == null) + { + return false; + } + return Store.Remove(key); + } + + /// <summary> + /// Retrieves a value from the dictionary. + /// </summary> + /// <param name="key">The header name.</param> + /// <param name="value">The value.</param> + /// <returns>true if the <see cref="HeaderDictionary" /> contains the key; otherwise, false.</returns> + public bool TryGetValue(string key, out StringValues value) + { + if (Store == null) + { + value = default(StringValues); + return false; + } + return Store.TryGetValue(key, out value); + } + + /// <summary> + /// Returns an enumerator that iterates through a collection. + /// </summary> + /// <returns>An <see cref="Enumerator" /> object that can be used to iterate through the collection.</returns> + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyEnumerator; + } + return new Enumerator(Store.GetEnumerator()); + } + + /// <summary> + /// Returns an enumerator that iterates through a collection. + /// </summary> + /// <returns>An <see cref="IEnumerator" /> object that can be used to iterate through the collection.</returns> + IEnumerator<KeyValuePair<string, StringValues>> IEnumerable<KeyValuePair<string, StringValues>>.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumeratorType; + } + return Store.GetEnumerator(); + } + + /// <summary> + /// Returns an enumerator that iterates through a collection. + /// </summary> + /// <returns>An <see cref="IEnumerator" /> object that can be used to iterate through the collection.</returns> + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + return Store.GetEnumerator(); + } + + private void ThrowIfReadOnly() + { + if (IsReadOnly) + { + throw new InvalidOperationException("The response headers cannot be modified because the response has already started."); + } + } + + public struct Enumerator : IEnumerator<KeyValuePair<string, StringValues>> + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary<string, StringValues>.Enumerator _dictionaryEnumerator; + private bool _notEmpty; + + internal Enumerator(Dictionary<string, StringValues>.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } + + public bool MoveNext() + { + if (_notEmpty) + { + return _dictionaryEnumerator.MoveNext(); + } + return false; + } + + public KeyValuePair<string, StringValues> Current + { + get + { + if (_notEmpty) + { + return _dictionaryEnumerator.Current; + } + return default(KeyValuePair<string, StringValues>); + } + } + + public void Dispose() + { + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + void IEnumerator.Reset() + { + if (_notEmpty) + { + ((IEnumerator)_dictionaryEnumerator).Reset(); + } + } + } + } +} diff --git a/src/Http/Http/src/HttpContextAccessor.cs b/src/Http/Http/src/HttpContextAccessor.cs new file mode 100644 index 0000000000000000000000000000000000000000..5a4676234cdb05f9a2e78a031d7f09837e11fd6a --- /dev/null +++ b/src/Http/Http/src/HttpContextAccessor.cs @@ -0,0 +1,24 @@ +// 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; + +namespace Microsoft.AspNetCore.Http +{ + public class HttpContextAccessor : IHttpContextAccessor + { + private static AsyncLocal<HttpContext> _httpContextCurrent = new AsyncLocal<HttpContext>(); + + public HttpContext HttpContext + { + get + { + return _httpContextCurrent.Value; + } + set + { + _httpContextCurrent.Value = value; + } + } + } +} diff --git a/src/Http/Http/src/HttpContextFactory.cs b/src/Http/Http/src/HttpContextFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..8236a388a564f1c3828a6724ad720bfbf2362e1c --- /dev/null +++ b/src/Http/Http/src/HttpContextFactory.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Http +{ + public class HttpContextFactory : IHttpContextFactory + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly FormOptions _formOptions; + + public HttpContextFactory(IOptions<FormOptions> formOptions) + : this(formOptions, httpContextAccessor: null) + { + } + + public HttpContextFactory(IOptions<FormOptions> formOptions, IHttpContextAccessor httpContextAccessor) + { + if (formOptions == null) + { + throw new ArgumentNullException(nameof(formOptions)); + } + + _formOptions = formOptions.Value; + _httpContextAccessor = httpContextAccessor; + } + + public HttpContext Create(IFeatureCollection featureCollection) + { + if (featureCollection == null) + { + throw new ArgumentNullException(nameof(featureCollection)); + } + + var httpContext = new DefaultHttpContext(featureCollection); + if (_httpContextAccessor != null) + { + _httpContextAccessor.HttpContext = httpContext; + } + + var formFeature = new FormFeature(httpContext.Request, _formOptions); + featureCollection.Set<IFormFeature>(formFeature); + + return httpContext; + } + + public void Dispose(HttpContext httpContext) + { + if (_httpContextAccessor != null) + { + _httpContextAccessor.HttpContext = null; + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/HttpServiceCollectionExtensions.cs b/src/Http/Http/src/HttpServiceCollectionExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..cccfe6d4e6f1723308ddcf2229b2b40dabe289f1 --- /dev/null +++ b/src/Http/Http/src/HttpServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// <summary> + /// Extension methods for configuring HttpContext services. + /// </summary> + public static class HttpServiceCollectionExtensions + { + /// <summary> + /// Adds a default implementation for the <see cref="IHttpContextAccessor"/> service. + /// </summary> + /// <param name="services">The <see cref="IServiceCollection"/>.</param> + /// <returns>The service collection.</returns> + public static IServiceCollection AddHttpContextAccessor(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); + return services; + } + } +} diff --git a/src/Http/Http/src/Internal/ApplicationBuilder.cs b/src/Http/Http/src/Internal/ApplicationBuilder.cs new file mode 100644 index 0000000000000000000000000000000000000000..d0b6b6f6bfc806824bd0a6545855958957ac47a8 --- /dev/null +++ b/src/Http/Http/src/Internal/ApplicationBuilder.cs @@ -0,0 +1,96 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Builder.Internal +{ + public class ApplicationBuilder : IApplicationBuilder + { + private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>(); + + public ApplicationBuilder(IServiceProvider serviceProvider) + { + Properties = new Dictionary<string, object>(StringComparer.Ordinal); + ApplicationServices = serviceProvider; + } + + public ApplicationBuilder(IServiceProvider serviceProvider, object server) + : this(serviceProvider) + { + SetProperty(Constants.BuilderProperties.ServerFeatures, server); + } + + private ApplicationBuilder(ApplicationBuilder builder) + { + Properties = new CopyOnWriteDictionary<string, object>(builder.Properties, StringComparer.Ordinal); + } + + public IServiceProvider ApplicationServices + { + get + { + return GetProperty<IServiceProvider>(Constants.BuilderProperties.ApplicationServices); + } + set + { + SetProperty<IServiceProvider>(Constants.BuilderProperties.ApplicationServices, value); + } + } + + public IFeatureCollection ServerFeatures + { + get + { + return GetProperty<IFeatureCollection>(Constants.BuilderProperties.ServerFeatures); + } + } + + public IDictionary<string, object> Properties { get; } + + private T GetProperty<T>(string key) + { + object value; + return Properties.TryGetValue(key, out value) ? (T)value : default(T); + } + + private void SetProperty<T>(string key, T value) + { + Properties[key] = value; + } + + public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware) + { + _components.Add(middleware); + return this; + } + + public IApplicationBuilder New() + { + return new ApplicationBuilder(this); + } + + public RequestDelegate Build() + { + RequestDelegate app = context => + { + context.Response.StatusCode = 404; + return Task.CompletedTask; + }; + + foreach (var component in _components.Reverse()) + { + app = component(app); + } + + return app; + } + } +} diff --git a/src/Http/Http/src/Internal/BindingAddress.cs b/src/Http/Http/src/Internal/BindingAddress.cs new file mode 100644 index 0000000000000000000000000000000000000000..492fa23dbe19971e6ee27059a9d76afe896a3eda --- /dev/null +++ b/src/Http/Http/src/Internal/BindingAddress.cs @@ -0,0 +1,155 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Globalization; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class BindingAddress + { + public string Host { get; private set; } + public string PathBase { get; private set; } + public int Port { get; internal set; } + public string Scheme { get; private set; } + + public bool IsUnixPipe + { + get + { + return Host.StartsWith(Constants.UnixPipeHostPrefix, StringComparison.Ordinal); + } + } + + public string UnixPipePath + { + get + { + if (!IsUnixPipe) + { + throw new InvalidOperationException("Binding address is not a unix pipe."); + } + + return Host.Substring(Constants.UnixPipeHostPrefix.Length - 1); + } + } + + public override string ToString() + { + if (IsUnixPipe) + { + return Scheme.ToLowerInvariant() + "://" + Host.ToLowerInvariant(); + } + else + { + return Scheme.ToLowerInvariant() + "://" + Host.ToLowerInvariant() + ":" + Port.ToString(CultureInfo.InvariantCulture) + PathBase.ToString(CultureInfo.InvariantCulture); + } + } + + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + + public override bool Equals(object obj) + { + var other = obj as BindingAddress; + if (other == null) + { + return false; + } + return string.Equals(Scheme, other.Scheme, StringComparison.OrdinalIgnoreCase) + && string.Equals(Host, other.Host, StringComparison.OrdinalIgnoreCase) + && Port == other.Port + && PathBase == other.PathBase; + } + + public static BindingAddress Parse(string address) + { + address = address ?? string.Empty; + + int schemeDelimiterStart = address.IndexOf("://", StringComparison.Ordinal); + if (schemeDelimiterStart < 0) + { + throw new FormatException($"Invalid url: '{address}'"); + } + int schemeDelimiterEnd = schemeDelimiterStart + "://".Length; + + var isUnixPipe = address.IndexOf(Constants.UnixPipeHostPrefix, schemeDelimiterEnd, StringComparison.Ordinal) == schemeDelimiterEnd; + + int pathDelimiterStart; + int pathDelimiterEnd; + if (!isUnixPipe) + { + pathDelimiterStart = address.IndexOf("/", schemeDelimiterEnd, StringComparison.Ordinal); + pathDelimiterEnd = pathDelimiterStart; + } + else + { + pathDelimiterStart = address.IndexOf(":", schemeDelimiterEnd + Constants.UnixPipeHostPrefix.Length, StringComparison.Ordinal); + pathDelimiterEnd = pathDelimiterStart + ":".Length; + } + + if (pathDelimiterStart < 0) + { + pathDelimiterStart = pathDelimiterEnd = address.Length; + } + + var serverAddress = new BindingAddress(); + serverAddress.Scheme = address.Substring(0, schemeDelimiterStart); + + var hasSpecifiedPort = false; + if (!isUnixPipe) + { + int portDelimiterStart = address.LastIndexOf(":", pathDelimiterStart - 1, pathDelimiterStart - schemeDelimiterEnd, StringComparison.Ordinal); + if (portDelimiterStart >= 0) + { + int portDelimiterEnd = portDelimiterStart + ":".Length; + + string portString = address.Substring(portDelimiterEnd, pathDelimiterStart - portDelimiterEnd); + int portNumber; + if (int.TryParse(portString, NumberStyles.Integer, CultureInfo.InvariantCulture, out portNumber)) + { + hasSpecifiedPort = true; + serverAddress.Host = address.Substring(schemeDelimiterEnd, portDelimiterStart - schemeDelimiterEnd); + serverAddress.Port = portNumber; + } + } + + if (!hasSpecifiedPort) + { + if (string.Equals(serverAddress.Scheme, "http", StringComparison.OrdinalIgnoreCase)) + { + serverAddress.Port = 80; + } + else if (string.Equals(serverAddress.Scheme, "https", StringComparison.OrdinalIgnoreCase)) + { + serverAddress.Port = 443; + } + } + } + + if (!hasSpecifiedPort) + { + serverAddress.Host = address.Substring(schemeDelimiterEnd, pathDelimiterStart - schemeDelimiterEnd); + } + + if (string.IsNullOrEmpty(serverAddress.Host)) + { + throw new FormatException($"Invalid url: '{address}'"); + } + + if (address[address.Length - 1] == '/') + { + serverAddress.PathBase = address.Substring(pathDelimiterEnd, address.Length - pathDelimiterEnd - 1); + } + else + { + serverAddress.PathBase = address.Substring(pathDelimiterEnd); + } + + return serverAddress; + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Internal/BufferingHelper.cs b/src/Http/Http/src/Internal/BufferingHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..b912f37116b28d5d9daaebb59fc8343a4839894d --- /dev/null +++ b/src/Http/Http/src/Internal/BufferingHelper.cs @@ -0,0 +1,80 @@ +// 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 Microsoft.AspNetCore.WebUtilities; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public static class BufferingHelper + { + internal const int DefaultBufferThreshold = 1024 * 30; + + private readonly static Func<string> _getTempDirectory = () => TempDirectory; + + private static string _tempDirectory; + + public static string TempDirectory + { + get + { + if (_tempDirectory == null) + { + // Look for folders in the following order. + var temp = Environment.GetEnvironmentVariable("ASPNETCORE_TEMP") ?? // ASPNETCORE_TEMP - User set temporary location. + Path.GetTempPath(); // Fall back. + + if (!Directory.Exists(temp)) + { + // TODO: ??? + throw new DirectoryNotFoundException(temp); + } + + _tempDirectory = temp; + } + + return _tempDirectory; + } + } + + public static HttpRequest EnableRewind(this HttpRequest request, int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + var body = request.Body; + if (!body.CanSeek) + { + var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, _getTempDirectory); + request.Body = fileStream; + request.HttpContext.Response.RegisterForDispose(fileStream); + } + return request; + } + + public static MultipartSection EnableRewind(this MultipartSection section, Action<IDisposable> registerForDispose, + int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null) + { + if (section == null) + { + throw new ArgumentNullException(nameof(section)); + } + if (registerForDispose == null) + { + throw new ArgumentNullException(nameof(registerForDispose)); + } + + var body = section.Body; + if (!body.CanSeek) + { + var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, _getTempDirectory); + section.Body = fileStream; + registerForDispose(fileStream); + } + return section; + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Internal/Constants.cs b/src/Http/Http/src/Internal/Constants.cs new file mode 100644 index 0000000000000000000000000000000000000000..280011b3e010b9d5bbd79519351c20f2b3ca5b2d --- /dev/null +++ b/src/Http/Http/src/Internal/Constants.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Internal +{ + internal static class Constants + { + internal const string Http = "http"; + internal const string Https = "https"; + internal const string UnixPipeHostPrefix = "unix:/"; + + internal static class BuilderProperties + { + internal static string ServerFeatures = "server.Features"; + internal static string ApplicationServices = "application.Services"; + } + } +} diff --git a/src/Http/Http/src/Internal/DefaultConnectionInfo.cs b/src/Http/Http/src/Internal/DefaultConnectionInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..6ae7f9fc383de31bf9aa47b18d03da44b72f04f6 --- /dev/null +++ b/src/Http/Http/src/Internal/DefaultConnectionInfo.cs @@ -0,0 +1,90 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class DefaultConnectionInfo : ConnectionInfo + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func<IFeatureCollection, IHttpConnectionFeature> _newHttpConnectionFeature = f => new HttpConnectionFeature(); + private readonly static Func<IFeatureCollection, ITlsConnectionFeature> _newTlsConnectionFeature = f => new TlsConnectionFeature(); + + private FeatureReferences<FeatureInterfaces> _features; + + public DefaultConnectionInfo(IFeatureCollection features) + { + Initialize(features); + } + + public virtual void Initialize( IFeatureCollection features) + { + _features = new FeatureReferences<FeatureInterfaces>(features); + } + + public virtual void Uninitialize() + { + _features = default(FeatureReferences<FeatureInterfaces>); + } + + private IHttpConnectionFeature HttpConnectionFeature => + _features.Fetch(ref _features.Cache.Connection, _newHttpConnectionFeature); + + private ITlsConnectionFeature TlsConnectionFeature=> + _features.Fetch(ref _features.Cache.TlsConnection, _newTlsConnectionFeature); + + /// <inheritdoc /> + public override string Id + { + get { return HttpConnectionFeature.ConnectionId; } + set { HttpConnectionFeature.ConnectionId = value; } + } + + public override IPAddress RemoteIpAddress + { + get { return HttpConnectionFeature.RemoteIpAddress; } + set { HttpConnectionFeature.RemoteIpAddress = value; } + } + + public override int RemotePort + { + get { return HttpConnectionFeature.RemotePort; } + set { HttpConnectionFeature.RemotePort = value; } + } + + public override IPAddress LocalIpAddress + { + get { return HttpConnectionFeature.LocalIpAddress; } + set { HttpConnectionFeature.LocalIpAddress = value; } + } + + public override int LocalPort + { + get { return HttpConnectionFeature.LocalPort; } + set { HttpConnectionFeature.LocalPort = value; } + } + + public override X509Certificate2 ClientCertificate + { + get { return TlsConnectionFeature.ClientCertificate; } + set { TlsConnectionFeature.ClientCertificate = value; } + } + + public override Task<X509Certificate2> GetClientCertificateAsync(CancellationToken cancellationToken = new CancellationToken()) + { + return TlsConnectionFeature.GetClientCertificateAsync(cancellationToken); + } + + struct FeatureInterfaces + { + public IHttpConnectionFeature Connection; + public ITlsConnectionFeature TlsConnection; + } + } +} diff --git a/src/Http/Http/src/Internal/DefaultHttpRequest.cs b/src/Http/Http/src/Internal/DefaultHttpRequest.cs new file mode 100644 index 0000000000000000000000000000000000000000..f216475db665bf3d28a498201779d8a2f808ba81 --- /dev/null +++ b/src/Http/Http/src/Internal/DefaultHttpRequest.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; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class DefaultHttpRequest : HttpRequest + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func<IFeatureCollection, IHttpRequestFeature> _nullRequestFeature = f => null; + private readonly static Func<IFeatureCollection, IQueryFeature> _newQueryFeature = f => new QueryFeature(f); + private readonly static Func<HttpRequest, IFormFeature> _newFormFeature = r => new FormFeature(r); + private readonly static Func<IFeatureCollection, IRequestCookiesFeature> _newRequestCookiesFeature = f => new RequestCookiesFeature(f); + + private HttpContext _context; + private FeatureReferences<FeatureInterfaces> _features; + + public DefaultHttpRequest(HttpContext context) + { + Initialize(context); + } + + public virtual void Initialize(HttpContext context) + { + _context = context; + _features = new FeatureReferences<FeatureInterfaces>(context.Features); + } + + public virtual void Uninitialize() + { + _context = null; + _features = default(FeatureReferences<FeatureInterfaces>); + } + + public override HttpContext HttpContext => _context; + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache.Request, _nullRequestFeature); + + private IQueryFeature QueryFeature => + _features.Fetch(ref _features.Cache.Query, _newQueryFeature); + + private IFormFeature FormFeature => + _features.Fetch(ref _features.Cache.Form, this, _newFormFeature); + + private IRequestCookiesFeature RequestCookiesFeature => + _features.Fetch(ref _features.Cache.Cookies, _newRequestCookiesFeature); + + public override PathString PathBase + { + get { return new PathString(HttpRequestFeature.PathBase); } + set { HttpRequestFeature.PathBase = value.Value; } + } + + public override PathString Path + { + get { return new PathString(HttpRequestFeature.Path); } + set { HttpRequestFeature.Path = value.Value; } + } + + public override QueryString QueryString + { + get { return new QueryString(HttpRequestFeature.QueryString); } + set { HttpRequestFeature.QueryString = value.Value; } + } + + public override long? ContentLength + { + get { return Headers.ContentLength; } + set { Headers.ContentLength = value; } + } + + public override Stream Body + { + get { return HttpRequestFeature.Body; } + set { HttpRequestFeature.Body = value; } + } + + public override string Method + { + get { return HttpRequestFeature.Method; } + set { HttpRequestFeature.Method = value; } + } + + public override string Scheme + { + get { return HttpRequestFeature.Scheme; } + set { HttpRequestFeature.Scheme = value; } + } + + public override bool IsHttps + { + get { return string.Equals(Constants.Https, Scheme, StringComparison.OrdinalIgnoreCase); } + set { Scheme = value ? Constants.Https : Constants.Http; } + } + + public override HostString Host + { + get { return HostString.FromUriComponent(Headers["Host"]); } + set { Headers["Host"] = value.ToUriComponent(); } + } + + public override IQueryCollection Query + { + get { return QueryFeature.Query; } + set { QueryFeature.Query = value; } + } + + public override string Protocol + { + get { return HttpRequestFeature.Protocol; } + set { HttpRequestFeature.Protocol = value; } + } + + public override IHeaderDictionary Headers + { + get { return HttpRequestFeature.Headers; } + } + + public override IRequestCookieCollection Cookies + { + get { return RequestCookiesFeature.Cookies; } + set { RequestCookiesFeature.Cookies = value; } + } + + public override string ContentType + { + get { return Headers[HeaderNames.ContentType]; } + set { Headers[HeaderNames.ContentType] = value; } + } + + public override bool HasFormContentType + { + get { return FormFeature.HasFormContentType; } + } + + public override IFormCollection Form + { + get { return FormFeature.ReadForm(); } + set { FormFeature.Form = value; } + } + + public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken) + { + return FormFeature.ReadFormAsync(cancellationToken); + } + + struct FeatureInterfaces + { + public IHttpRequestFeature Request; + public IQueryFeature Query; + public IFormFeature Form; + public IRequestCookiesFeature Cookies; + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Internal/DefaultHttpResponse.cs b/src/Http/Http/src/Internal/DefaultHttpResponse.cs new file mode 100644 index 0000000000000000000000000000000000000000..3ca05035f5c04dd22e2ea9d71e2fa2a8a13b4720 --- /dev/null +++ b/src/Http/Http/src/Internal/DefaultHttpResponse.cs @@ -0,0 +1,139 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class DefaultHttpResponse : HttpResponse + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func<IFeatureCollection, IHttpResponseFeature> _nullResponseFeature = f => null; + private readonly static Func<IFeatureCollection, IResponseCookiesFeature> _newResponseCookiesFeature = f => new ResponseCookiesFeature(f); + + private HttpContext _context; + private FeatureReferences<FeatureInterfaces> _features; + + public DefaultHttpResponse(HttpContext context) + { + Initialize(context); + } + + public virtual void Initialize(HttpContext context) + { + _context = context; + _features = new FeatureReferences<FeatureInterfaces>(context.Features); + } + + public virtual void Uninitialize() + { + _context = null; + _features = default(FeatureReferences<FeatureInterfaces>); + } + + private IHttpResponseFeature HttpResponseFeature => + _features.Fetch(ref _features.Cache.Response, _nullResponseFeature); + + private IResponseCookiesFeature ResponseCookiesFeature => + _features.Fetch(ref _features.Cache.Cookies, _newResponseCookiesFeature); + + + public override HttpContext HttpContext { get { return _context; } } + + public override int StatusCode + { + get { return HttpResponseFeature.StatusCode; } + set { HttpResponseFeature.StatusCode = value; } + } + + public override IHeaderDictionary Headers + { + get { return HttpResponseFeature.Headers; } + } + + public override Stream Body + { + get { return HttpResponseFeature.Body; } + set { HttpResponseFeature.Body = value; } + } + + public override long? ContentLength + { + get { return Headers.ContentLength; } + set { Headers.ContentLength = value; } + } + + public override string ContentType + { + get + { + return Headers[HeaderNames.ContentType]; + } + set + { + if (string.IsNullOrEmpty(value)) + { + HttpResponseFeature.Headers.Remove(HeaderNames.ContentType); + } + else + { + HttpResponseFeature.Headers[HeaderNames.ContentType] = value; + } + } + } + + public override IResponseCookies Cookies + { + get { return ResponseCookiesFeature.Cookies; } + } + + public override bool HasStarted + { + get { return HttpResponseFeature.HasStarted; } + } + + public override void OnStarting(Func<object, Task> callback, object state) + { + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + HttpResponseFeature.OnStarting(callback, state); + } + + public override void OnCompleted(Func<object, Task> callback, object state) + { + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + HttpResponseFeature.OnCompleted(callback, state); + } + + public override void Redirect(string location, bool permanent) + { + if (permanent) + { + HttpResponseFeature.StatusCode = 301; + } + else + { + HttpResponseFeature.StatusCode = 302; + } + + Headers[HeaderNames.Location] = location; + } + + struct FeatureInterfaces + { + public IHttpResponseFeature Response; + public IResponseCookiesFeature Cookies; + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Internal/DefaultWebSocketManager.cs b/src/Http/Http/src/Internal/DefaultWebSocketManager.cs new file mode 100644 index 0000000000000000000000000000000000000000..477282408d7cad42199542dba8f4fd0949fc83ca --- /dev/null +++ b/src/Http/Http/src/Internal/DefaultWebSocketManager.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class DefaultWebSocketManager : WebSocketManager + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func<IFeatureCollection, IHttpRequestFeature> _nullRequestFeature = f => null; + private readonly static Func<IFeatureCollection, IHttpWebSocketFeature> _nullWebSocketFeature = f => null; + + private FeatureReferences<FeatureInterfaces> _features; + + public DefaultWebSocketManager(IFeatureCollection features) + { + Initialize(features); + } + + public virtual void Initialize(IFeatureCollection features) + { + _features = new FeatureReferences<FeatureInterfaces>(features); + } + + public virtual void Uninitialize() + { + _features = default(FeatureReferences<FeatureInterfaces>); + } + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache.Request, _nullRequestFeature); + + private IHttpWebSocketFeature WebSocketFeature => + _features.Fetch(ref _features.Cache.WebSockets, _nullWebSocketFeature); + + public override bool IsWebSocketRequest + { + get + { + return WebSocketFeature != null && WebSocketFeature.IsWebSocketRequest; + } + } + + public override IList<string> WebSocketRequestedProtocols + { + get + { + return ParsingHelpers.GetHeaderSplit(HttpRequestFeature.Headers, HeaderNames.WebSocketSubProtocols); + } + } + + public override Task<WebSocket> AcceptWebSocketAsync(string subProtocol) + { + if (WebSocketFeature == null) + { + throw new NotSupportedException("WebSockets are not supported"); + } + return WebSocketFeature.AcceptAsync(new WebSocketAcceptContext() { SubProtocol = subProtocol }); + } + + struct FeatureInterfaces + { + public IHttpRequestFeature Request; + public IHttpWebSocketFeature WebSockets; + } + } +} diff --git a/src/Http/Http/src/Internal/FormFile.cs b/src/Http/Http/src/Internal/FormFile.cs new file mode 100644 index 0000000000000000000000000000000000000000..b4a3f4d91f40082e62e062c3db993db45adce0fb --- /dev/null +++ b/src/Http/Http/src/Internal/FormFile.cs @@ -0,0 +1,109 @@ +// 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.Http.Internal +{ + public class FormFile : IFormFile + { + // Stream.CopyTo method uses 80KB as the default buffer size. + private const int DefaultBufferSize = 80 * 1024; + + private readonly Stream _baseStream; + private readonly long _baseStreamOffset; + + public FormFile(Stream baseStream, long baseStreamOffset, long length, string name, string fileName) + { + _baseStream = baseStream; + _baseStreamOffset = baseStreamOffset; + Length = length; + Name = name; + FileName = fileName; + } + + /// <summary> + /// Gets the raw Content-Disposition header of the uploaded file. + /// </summary> + public string ContentDisposition + { + get { return Headers["Content-Disposition"]; } + set { Headers["Content-Disposition"] = value; } + } + + /// <summary> + /// Gets the raw Content-Type header of the uploaded file. + /// </summary> + public string ContentType + { + get { return Headers["Content-Type"]; } + set { Headers["Content-Type"] = value; } + } + + /// <summary> + /// Gets the header dictionary of the uploaded file. + /// </summary> + public IHeaderDictionary Headers { get; set; } + + /// <summary> + /// Gets the file length in bytes. + /// </summary> + public long Length { get; } + + /// <summary> + /// Gets the name from the Content-Disposition header. + /// </summary> + public string Name { get; } + + /// <summary> + /// Gets the file name from the Content-Disposition header. + /// </summary> + public string FileName { get; } + + /// <summary> + /// Opens the request stream for reading the uploaded file. + /// </summary> + public Stream OpenReadStream() + { + return new ReferenceReadStream(_baseStream, _baseStreamOffset, Length); + } + + /// <summary> + /// Copies the contents of the uploaded file to the <paramref name="target"/> stream. + /// </summary> + /// <param name="target">The stream to copy the file contents to.</param> + public void CopyTo(Stream target) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + using (var readStream = OpenReadStream()) + { + readStream.CopyTo(target, DefaultBufferSize); + } + } + + /// <summary> + /// Asynchronously copies the contents of the uploaded file to the <paramref name="target"/> stream. + /// </summary> + /// <param name="target">The stream to copy the file contents to.</param> + /// <param name="cancellationToken"></param> + public async Task CopyToAsync(Stream target, CancellationToken cancellationToken = default(CancellationToken)) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + using (var readStream = OpenReadStream()) + { + await readStream.CopyToAsync(target, DefaultBufferSize, cancellationToken); + } + } + } +} diff --git a/src/Http/Http/src/Internal/FormFileCollection.cs b/src/Http/Http/src/Internal/FormFileCollection.cs new file mode 100644 index 0000000000000000000000000000000000000000..806e756a8e06f64c717402ee23e35226623936b8 --- /dev/null +++ b/src/Http/Http/src/Internal/FormFileCollection.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class FormFileCollection : List<IFormFile>, IFormFileCollection + { + public IFormFile this[string name] => GetFile(name); + + public IFormFile GetFile(string name) + { + foreach (var file in this) + { + if (string.Equals(name, file.Name, StringComparison.OrdinalIgnoreCase)) + { + return file; + } + } + + return null; + } + + public IReadOnlyList<IFormFile> GetFiles(string name) + { + var files = new List<IFormFile>(); + + foreach (var file in this) + { + if (string.Equals(name, file.Name, StringComparison.OrdinalIgnoreCase)) + { + files.Add(file); + } + } + + return files; + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Internal/ItemsDictionary.cs b/src/Http/Http/src/Internal/ItemsDictionary.cs new file mode 100644 index 0000000000000000000000000000000000000000..4821912240c075e6402b731312d0e35833d6c213 --- /dev/null +++ b/src/Http/Http/src/Internal/ItemsDictionary.cs @@ -0,0 +1,118 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class ItemsDictionary : IDictionary<object, object> + { + public ItemsDictionary() + : this(new Dictionary<object, object>()) + { + } + + public ItemsDictionary(IDictionary<object, object> items) + { + Items = items; + } + + public IDictionary<object, object> Items { get; } + + // Replace the indexer with one that returns null for missing values + object IDictionary<object, object>.this[object key] + { + get + { + object value; + if (Items.TryGetValue(key, out value)) + { + return value; + } + return null; + } + set { Items[key] = value; } + } + + void IDictionary<object, object>.Add(object key, object value) + { + Items.Add(key, value); + } + + bool IDictionary<object, object>.ContainsKey(object key) + { + return Items.ContainsKey(key); + } + + ICollection<object> IDictionary<object, object>.Keys + { + get { return Items.Keys; } + } + + bool IDictionary<object, object>.Remove(object key) + { + return Items.Remove(key); + } + + bool IDictionary<object, object>.TryGetValue(object key, out object value) + { + return Items.TryGetValue(key, out value); + } + + ICollection<object> IDictionary<object, object>.Values + { + get { return Items.Values; } + } + + void ICollection<KeyValuePair<object, object>>.Add(KeyValuePair<object, object> item) + { + Items.Add(item); + } + + void ICollection<KeyValuePair<object, object>>.Clear() + { + Items.Clear(); + } + + bool ICollection<KeyValuePair<object, object>>.Contains(KeyValuePair<object, object> item) + { + return Items.Contains(item); + } + + void ICollection<KeyValuePair<object, object>>.CopyTo(KeyValuePair<object, object>[] array, int arrayIndex) + { + Items.CopyTo(array, arrayIndex); + } + + int ICollection<KeyValuePair<object, object>>.Count + { + get { return Items.Count; } + } + + bool ICollection<KeyValuePair<object, object>>.IsReadOnly + { + get { return Items.IsReadOnly; } + } + + bool ICollection<KeyValuePair<object, object>>.Remove(KeyValuePair<object, object> item) + { + object value; + if (Items.TryGetValue(item.Key, out value) && Equals(item.Value, value)) + { + return Items.Remove(item.Key); + } + return false; + } + + IEnumerator<KeyValuePair<object, object>> IEnumerable<KeyValuePair<object, object>>.GetEnumerator() + { + return Items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return Items.GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Internal/QueryCollection.cs b/src/Http/Http/src/Internal/QueryCollection.cs new file mode 100644 index 0000000000000000000000000000000000000000..620de44a9255a299b83d14e89a4f196a13076e26 --- /dev/null +++ b/src/Http/Http/src/Internal/QueryCollection.cs @@ -0,0 +1,222 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http.Internal +{ + /// <summary> + /// The HttpRequest query string collection + /// </summary> + public class QueryCollection : IQueryCollection + { + public static readonly QueryCollection Empty = new QueryCollection(); + private static readonly string[] EmptyKeys = Array.Empty<string>(); + private static readonly StringValues[] EmptyValues = Array.Empty<StringValues>(); + private static readonly Enumerator EmptyEnumerator = new Enumerator(); + // Pre-box + private static readonly IEnumerator<KeyValuePair<string, StringValues>> EmptyIEnumeratorType = EmptyEnumerator; + private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; + + private Dictionary<string, StringValues> Store { get; set; } + + public QueryCollection() + { + } + + public QueryCollection(Dictionary<string, StringValues> store) + { + Store = store; + } + + public QueryCollection(QueryCollection store) + { + Store = store.Store; + } + + public QueryCollection(int capacity) + { + Store = new Dictionary<string, StringValues>(capacity, StringComparer.OrdinalIgnoreCase); + } + + /// <summary> + /// Get or sets the associated value from the collection as a single string. + /// </summary> + /// <param name="key">The key name.</param> + /// <returns>the associated value from the collection as a StringValues or StringValues.Empty if the key is not present.</returns> + public StringValues this[string key] + { + get + { + if (Store == null) + { + return StringValues.Empty; + } + + StringValues value; + if (TryGetValue(key, out value)) + { + return value; + } + return StringValues.Empty; + } + } + + /// <summary> + /// Gets the number of elements contained in the <see cref="QueryCollection" />;. + /// </summary> + /// <returns>The number of elements contained in the <see cref="QueryCollection" />.</returns> + public int Count + { + get + { + if (Store == null) + { + return 0; + } + return Store.Count; + } + } + + public ICollection<string> Keys + { + get + { + if (Store == null) + { + return EmptyKeys; + } + return Store.Keys; + } + } + + /// <summary> + /// Determines whether the <see cref="QueryCollection" /> contains a specific key. + /// </summary> + /// <param name="key">The key.</param> + /// <returns>true if the <see cref="QueryCollection" /> contains a specific key; otherwise, false.</returns> + public bool ContainsKey(string key) + { + if (Store == null) + { + return false; + } + return Store.ContainsKey(key); + } + + /// <summary> + /// Retrieves a value from the collection. + /// </summary> + /// <param name="key">The key.</param> + /// <param name="value">The value.</param> + /// <returns>true if the <see cref="QueryCollection" /> contains the key; otherwise, false.</returns> + public bool TryGetValue(string key, out StringValues value) + { + if (Store == null) + { + value = default(StringValues); + return false; + } + return Store.TryGetValue(key, out value); + } + + /// <summary> + /// Returns an enumerator that iterates through a collection. + /// </summary> + /// <returns>An <see cref="Enumerator" /> object that can be used to iterate through the collection.</returns> + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyEnumerator; + } + return new Enumerator(Store.GetEnumerator()); + } + + /// <summary> + /// Returns an enumerator that iterates through a collection. + /// </summary> + /// <returns>An <see cref="IEnumerator{T}" /> object that can be used to iterate through the collection.</returns> + IEnumerator<KeyValuePair<string, StringValues>> IEnumerable<KeyValuePair<string, StringValues>>.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumeratorType; + } + return Store.GetEnumerator(); + } + + /// <summary> + /// Returns an enumerator that iterates through a collection. + /// </summary> + /// <returns>An <see cref="IEnumerator" /> object that can be used to iterate through the collection.</returns> + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + return Store.GetEnumerator(); + } + + public struct Enumerator : IEnumerator<KeyValuePair<string, StringValues>> + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary<string, StringValues>.Enumerator _dictionaryEnumerator; + private bool _notEmpty; + + internal Enumerator(Dictionary<string, StringValues>.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } + + public bool MoveNext() + { + if (_notEmpty) + { + return _dictionaryEnumerator.MoveNext(); + } + return false; + } + + public KeyValuePair<string, StringValues> Current + { + get + { + if (_notEmpty) + { + return _dictionaryEnumerator.Current; + } + return default(KeyValuePair<string, StringValues>); + } + } + + public void Dispose() + { + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + void IEnumerator.Reset() + { + if (_notEmpty) + { + ((IEnumerator)_dictionaryEnumerator).Reset(); + } + } + } + } +} diff --git a/src/Http/Http/src/Internal/ReferenceReadStream.cs b/src/Http/Http/src/Internal/ReferenceReadStream.cs new file mode 100644 index 0000000000000000000000000000000000000000..c36a59d01020cf8927c8a8dc0f3889d5a9decbf4 --- /dev/null +++ b/src/Http/Http/src/Internal/ReferenceReadStream.cs @@ -0,0 +1,154 @@ +// 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.Http.Internal +{ + /// <summary> + /// A Stream that wraps another stream starting at a certain offset and reading for the given length. + /// </summary> + internal class ReferenceReadStream : Stream + { + private readonly Stream _inner; + private readonly long _innerOffset; + private readonly long _length; + private long _position; + + private bool _disposed; + + public ReferenceReadStream(Stream inner, long offset, long length) + { + if (inner == null) + { + throw new ArgumentNullException(nameof(inner)); + } + + _inner = inner; + _innerOffset = offset; + _length = length; + _inner.Position = offset; + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return _inner.CanSeek; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get { return _position; } + set + { + ThrowIfDisposed(); + if (value < 0 || value > Length) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "The Position must be within the length of the Stream: " + Length.ToString()); + } + VerifyPosition(); + _position = value; + _inner.Position = _innerOffset + _position; + } + } + + // Throws if the position in the underlying stream has changed without our knowledge, indicating someone else is trying + // to use the stream at the same time which could lead to data corruption. + private void VerifyPosition() + { + if (_inner.Position != _innerOffset + _position) + { + throw new InvalidOperationException("The inner stream position has changed unexpectedly."); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + Position = offset; + } + else if (origin == SeekOrigin.End) + { + Position = Length + offset; + } + else // if (origin == SeekOrigin.Current) + { + Position = Position + offset; + } + return Position; + } + + public override int Read(byte[] buffer, int offset, int count) + { + ThrowIfDisposed(); + VerifyPosition(); + var toRead = Math.Min(count, _length - _position); + var read = _inner.Read(buffer, offset, (int)toRead); + _position += read; + return read; + } + + public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + VerifyPosition(); + var toRead = Math.Min(count, _length - _position); + var read = await _inner.ReadAsync(buffer, offset, (int)toRead, cancellationToken); + _position += read; + return read; + } + + 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 SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _disposed = true; + } + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(ReferenceReadStream)); + } + } + } +} diff --git a/src/Http/Http/src/Internal/RequestCookieCollection.cs b/src/Http/Http/src/Internal/RequestCookieCollection.cs new file mode 100644 index 0000000000000000000000000000000000000000..4af0a652463c474a16b950492542f6cb3d827177 --- /dev/null +++ b/src/Http/Http/src/Internal/RequestCookieCollection.cs @@ -0,0 +1,232 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class RequestCookieCollection : IRequestCookieCollection + { + public static readonly RequestCookieCollection Empty = new RequestCookieCollection(); + private static readonly string[] EmptyKeys = Array.Empty<string>(); + private static readonly Enumerator EmptyEnumerator = new Enumerator(); + // Pre-box + private static readonly IEnumerator<KeyValuePair<string, string>> EmptyIEnumeratorType = EmptyEnumerator; + private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; + + private Dictionary<string, string> Store { get; set; } + + public RequestCookieCollection() + { + } + + public RequestCookieCollection(Dictionary<string, string> store) + { + Store = store; + } + + public RequestCookieCollection(int capacity) + { + Store = new Dictionary<string, string>(capacity, StringComparer.OrdinalIgnoreCase); + } + + public string this[string key] + { + get + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (Store == null) + { + return null; + } + + string value; + if (TryGetValue(key, out value)) + { + return value; + } + return null; + } + } + + public static RequestCookieCollection Parse(IList<string> values) + { + if (values.Count == 0) + { + return Empty; + } + + IList<CookieHeaderValue> cookies; + if (CookieHeaderValue.TryParseList(values, out cookies)) + { + if (cookies.Count == 0) + { + return Empty; + } + + var collection = new RequestCookieCollection(cookies.Count); + var store = collection.Store; + for (var i = 0; i < cookies.Count; i++) + { + var cookie = cookies[i]; + var name = Uri.UnescapeDataString(cookie.Name.Value); + var value = Uri.UnescapeDataString(cookie.Value.Value); + store[name] = value; + } + + return collection; + } + return Empty; + } + + public int Count + { + get + { + if (Store == null) + { + return 0; + } + return Store.Count; + } + } + + public ICollection<string> Keys + { + get + { + if (Store == null) + { + return EmptyKeys; + } + return Store.Keys; + } + } + + public bool ContainsKey(string key) + { + if (Store == null) + { + return false; + } + return Store.ContainsKey(key); + } + + public bool TryGetValue(string key, out string value) + { + if (Store == null) + { + value = null; + return false; + } + return Store.TryGetValue(key, out value); + } + + /// <summary> + /// Returns an struct enumerator that iterates through a collection without boxing. + /// </summary> + /// <returns>An <see cref="Enumerator" /> object that can be used to iterate through the collection.</returns> + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyEnumerator; + } + // Non-boxed Enumerator + return new Enumerator(Store.GetEnumerator()); + } + + /// <summary> + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// </summary> + /// <returns>An <see cref="IEnumerator{T}" /> object that can be used to iterate through the collection.</returns> + IEnumerator<KeyValuePair<string, string>> IEnumerable<KeyValuePair<string, string>>.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumeratorType; + } + // Boxed Enumerator + return GetEnumerator(); + } + + /// <summary> + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// </summary> + /// <returns>An <see cref="IEnumerator" /> object that can be used to iterate through the collection.</returns> + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + // Boxed Enumerator + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator<KeyValuePair<string, string>> + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary<string, string>.Enumerator _dictionaryEnumerator; + private bool _notEmpty; + + internal Enumerator(Dictionary<string, string>.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } + + public bool MoveNext() + { + if (_notEmpty) + { + return _dictionaryEnumerator.MoveNext(); + } + return false; + } + + public KeyValuePair<string, string> Current + { + get + { + if (_notEmpty) + { + var current = _dictionaryEnumerator.Current; + return new KeyValuePair<string, string>(current.Key, current.Value); + } + return default(KeyValuePair<string, string>); + } + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + public void Dispose() + { + } + + public void Reset() + { + if (_notEmpty) + { + ((IEnumerator)_dictionaryEnumerator).Reset(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Internal/ResponseCookies.cs b/src/Http/Http/src/Internal/ResponseCookies.cs new file mode 100644 index 0000000000000000000000000000000000000000..7c6e3e033ba04f133d7d1e2222ed26a175db1fb1 --- /dev/null +++ b/src/Http/Http/src/Internal/ResponseCookies.cs @@ -0,0 +1,139 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + /// <summary> + /// A wrapper for the response Set-Cookie header. + /// </summary> + public class ResponseCookies : IResponseCookies + { + /// <summary> + /// Create a new wrapper. + /// </summary> + /// <param name="headers">The <see cref="IHeaderDictionary"/> for the response.</param> + /// <param name="builderPool">The <see cref="ObjectPool{T}"/>, if available.</param> + public ResponseCookies(IHeaderDictionary headers, ObjectPool<StringBuilder> builderPool) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + Headers = headers; + } + + private IHeaderDictionary Headers { get; set; } + + /// <inheritdoc /> + public void Append(string key, string value) + { + var setCookieHeaderValue = new SetCookieHeaderValue( + Uri.EscapeDataString(key), + Uri.EscapeDataString(value)) + { + Path = "/" + }; + var cookieValue = setCookieHeaderValue.ToString(); + + Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], cookieValue); + } + + /// <inheritdoc /> + public void Append(string key, string value, CookieOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + var setCookieHeaderValue = new SetCookieHeaderValue( + Uri.EscapeDataString(key), + Uri.EscapeDataString(value)) + { + Domain = options.Domain, + Path = options.Path, + Expires = options.Expires, + MaxAge = options.MaxAge, + Secure = options.Secure, + SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite, + HttpOnly = options.HttpOnly + }; + + var cookieValue = setCookieHeaderValue.ToString(); + + Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], cookieValue); + } + + /// <inheritdoc /> + public void Delete(string key) + { + Delete(key, new CookieOptions() { Path = "/" }); + } + + /// <inheritdoc /> + public void Delete(string key, CookieOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + var encodedKeyPlusEquals = Uri.EscapeDataString(key) + "="; + bool domainHasValue = !string.IsNullOrEmpty(options.Domain); + bool pathHasValue = !string.IsNullOrEmpty(options.Path); + + Func<string, string, CookieOptions, bool> rejectPredicate; + if (domainHasValue) + { + rejectPredicate = (value, encKeyPlusEquals, opts) => + value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && + value.IndexOf($"domain={opts.Domain}", StringComparison.OrdinalIgnoreCase) != -1; + } + else if (pathHasValue) + { + rejectPredicate = (value, encKeyPlusEquals, opts) => + value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && + value.IndexOf($"path={opts.Path}", StringComparison.OrdinalIgnoreCase) != -1; + } + else + { + rejectPredicate = (value, encKeyPlusEquals, opts) => value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase); + } + + var existingValues = Headers[HeaderNames.SetCookie]; + if (!StringValues.IsNullOrEmpty(existingValues)) + { + var values = existingValues.ToArray(); + var newValues = new List<string>(); + + for (var i = 0; i < values.Length; i++) + { + if (!rejectPredicate(values[i], encodedKeyPlusEquals, options)) + { + newValues.Add(values[i]); + } + } + + Headers[HeaderNames.SetCookie] = new StringValues(newValues.ToArray()); + } + + Append(key, string.Empty, new CookieOptions + { + Path = options.Path, + Domain = options.Domain, + Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + Secure = options.Secure, + HttpOnly = options.HttpOnly, + SameSite = options.SameSite + }); + } + } +} diff --git a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj new file mode 100644 index 0000000000000000000000000000000000000000..4344d0ae8eabf0d0ca145732f9b7b2144c32471a --- /dev/null +++ b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj @@ -0,0 +1,21 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>ASP.NET Core default HTTP feature implementations.</Description> + <TargetFramework>netstandard2.0</TargetFramework> + <NoWarn>$(NoWarn);CS1591</NoWarn> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore</PackageTags> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Http.Abstractions" /> + <Reference Include="Microsoft.AspNetCore.WebUtilities" /> + <Reference Include="Microsoft.Extensions.CopyOnWriteDictionary.Sources" PrivateAssets="All" /> + <Reference Include="Microsoft.Extensions.ObjectPool" /> + <Reference Include="Microsoft.Extensions.Options" /> + <Reference Include="Microsoft.Net.Http.Headers" /> + </ItemGroup> + +</Project> diff --git a/src/Http/Http/src/MiddlewareFactory.cs b/src/Http/Http/src/MiddlewareFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..5e5cd285f4450511fe87c7037e0e8a5b5332fefa --- /dev/null +++ b/src/Http/Http/src/MiddlewareFactory.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Http +{ + public class MiddlewareFactory : IMiddlewareFactory + { + // The default middleware factory is just an IServiceProvider proxy. + // This should be registered as a scoped service so that the middleware instances + // don't end up being singletons. + private readonly IServiceProvider _serviceProvider; + + public MiddlewareFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public IMiddleware Create(Type middlewareType) + { + return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware; + } + + public void Release(IMiddleware middleware) + { + // The container owns the lifetime of the service + } + } +} diff --git a/src/Http/Http/src/RequestFormReaderExtensions.cs b/src/Http/Http/src/RequestFormReaderExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..8675ad7f8c31bf82d8f801697330735c219161d5 --- /dev/null +++ b/src/Http/Http/src/RequestFormReaderExtensions.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http +{ + public static class RequestFormReaderExtensions + { + /// <summary> + /// Read the request body as a form with the given options. These options will only be used + /// if the form has not already been read. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="options">Options for reading the form.</param> + /// <param name="cancellationToken"></param> + /// <returns>The parsed form.</returns> + public static Task<IFormCollection> ReadFormAsync(this HttpRequest request, FormOptions options, + CancellationToken cancellationToken = new CancellationToken()) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (!request.HasFormContentType) + { + throw new InvalidOperationException("Incorrect Content-Type: " + request.ContentType); + } + + var features = request.HttpContext.Features; + var formFeature = features.Get<IFormFeature>(); + if (formFeature == null || formFeature.Form == null) + { + // We haven't read the form yet, replace the reader with one using our own options. + features.Set<IFormFeature>(new FormFeature(request, options)); + } + return request.ReadFormAsync(cancellationToken); + } + } +} diff --git a/src/Http/Http/src/baseline.netcore.json b/src/Http/Http/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..932bd2b6e45303e32a5f503074807efb35e0ceb5 --- /dev/null +++ b/src/Http/Http/src/baseline.netcore.json @@ -0,0 +1,2783 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Http, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Http.DefaultHttpContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Http.HttpContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Initialize", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Uninitialize", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Features", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Request", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpRequest", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Response", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpResponse", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Connection", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.ConnectionInfo", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Authentication", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Authentication.AuthenticationManager", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_WebSockets", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.WebSocketManager", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_User", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Items", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.Object, System.Object>", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Items", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary<System.Object, System.Object>" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequestServices", + "Parameters": [], + "ReturnType": "System.IServiceProvider", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestServices", + "Parameters": [ + { + "Name": "value", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequestAborted", + "Parameters": [], + "ReturnType": "System.Threading.CancellationToken", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestAborted", + "Parameters": [ + { + "Name": "value", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TraceIdentifier", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TraceIdentifier", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Session", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.ISession", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Session", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.ISession" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Abort", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeHttpRequest", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpRequest", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UninitializeHttpRequest", + "Parameters": [ + { + "Name": "instance", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeHttpResponse", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpResponse", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UninitializeHttpResponse", + "Parameters": [ + { + "Name": "instance", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeConnectionInfo", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.ConnectionInfo", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UninitializeConnectionInfo", + "Parameters": [ + { + "Name": "instance", + "Type": "Microsoft.AspNetCore.Http.ConnectionInfo" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeAuthenticationManager", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Authentication.AuthenticationManager", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UninitializeAuthenticationManager", + "Parameters": [ + { + "Name": "instance", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationManager" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeWebSocketManager", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.WebSocketManager", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UninitializeWebSocketManager", + "Parameters": [ + { + "Name": "instance", + "Type": "Microsoft.AspNetCore.Http.WebSocketManager" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpRequestRewindExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "EnableBuffering", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EnableBuffering", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + }, + { + "Name": "bufferThreshold", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EnableBuffering", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + }, + { + "Name": "bufferLimit", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EnableBuffering", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + }, + { + "Name": "bufferThreshold", + "Type": "System.Int32" + }, + { + "Name": "bufferLimit", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.FormCollection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.IFormCollection" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Files", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IFormFileCollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IFormCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringValues", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IFormCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Count", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IFormCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Keys", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection<System.String>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IFormCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ContainsKey", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IFormCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetValue", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IFormCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.FormCollection+Enumerator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "fields", + "Type": "System.Collections.Generic.Dictionary<System.String, Microsoft.Extensions.Primitives.StringValues>" + }, + { + "Name": "files", + "Type": "Microsoft.AspNetCore.Http.IFormFileCollection", + "DefaultValue": "null" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Empty", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.FormCollection", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HeaderDictionary", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.IHeaderDictionary" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Keys", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection<System.String>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary<System.String, Microsoft.Extensions.Primitives.StringValues>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Values", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection<Microsoft.Extensions.Primitives.StringValues>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary<System.String, Microsoft.Extensions.Primitives.StringValues>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ContainsKey", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary<System.String, Microsoft.Extensions.Primitives.StringValues>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary<System.String, Microsoft.Extensions.Primitives.StringValues>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary<System.String, Microsoft.Extensions.Primitives.StringValues>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetValue", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary<System.String, Microsoft.Extensions.Primitives.StringValues>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Count", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsReadOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "item", + "Type": "System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Clear", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Contains", + "Parameters": [ + { + "Name": "item", + "Type": "System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CopyTo", + "Parameters": [ + { + "Name": "array", + "Type": "System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>[]" + }, + { + "Name": "arrayIndex", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "item", + "Type": "System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringValues", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentLength", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.Int64>" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsReadOnly", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HeaderDictionary+Enumerator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "store", + "Type": "System.Collections.Generic.Dictionary<System.String, Microsoft.Extensions.Primitives.StringValues>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "capacity", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpContextAccessor", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.IHttpContextAccessor" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHttpContextAccessor", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HttpContext", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHttpContextAccessor", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpContextFactory", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.IHttpContextFactory" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Create", + "Parameters": [ + { + "Name": "featureCollection", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHttpContextFactory", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHttpContextFactory", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "formOptions", + "Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Http.Features.FormOptions>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "formOptions", + "Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Http.Features.FormOptions>" + }, + { + "Name": "httpContextAccessor", + "Type": "Microsoft.AspNetCore.Http.IHttpContextAccessor" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.MiddlewareFactory", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.IMiddlewareFactory" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Create", + "Parameters": [ + { + "Name": "middlewareType", + "Type": "System.Type" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.IMiddleware", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IMiddlewareFactory", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Release", + "Parameters": [ + { + "Name": "middleware", + "Type": "Microsoft.AspNetCore.Http.IMiddleware" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IMiddlewareFactory", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "serviceProvider", + "Type": "System.IServiceProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.RequestFormReaderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "ReadFormAsync", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.Features.FormOptions" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Http.IFormCollection>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.DefaultSessionFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.ISessionFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Session", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.ISession", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.ISessionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Session", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.ISession" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.ISessionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.FormFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IFormFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_HasFormContentType", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFormFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Form", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IFormCollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFormFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Form", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IFormCollection" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFormFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadForm", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IFormCollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFormFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadFormAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Http.IFormCollection>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadFormAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Http.IFormCollection>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFormFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "form", + "Type": "Microsoft.AspNetCore.Http.IFormCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.Features.FormOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.FormOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_BufferBody", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BufferBody", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MemoryBufferThreshold", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MemoryBufferThreshold", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_BufferBodyLengthLimit", + "Parameters": [], + "ReturnType": "System.Int64", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BufferBodyLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ValueCountLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ValueCountLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_KeyLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_KeyLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ValueLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ValueLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MultipartBoundaryLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MultipartBoundaryLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MultipartHeadersCountLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MultipartHeadersCountLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MultipartHeadersLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MultipartHeadersLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MultipartBodyLengthLimit", + "Parameters": [], + "ReturnType": "System.Int64", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MultipartBodyLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "DefaultMemoryBufferThreshold", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "65536" + }, + { + "Kind": "Field", + "Name": "DefaultBufferBodyLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "134217728" + }, + { + "Kind": "Field", + "Name": "DefaultMultipartBoundaryLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "128" + }, + { + "Kind": "Field", + "Name": "DefaultMultipartBodyLengthLimit", + "Parameters": [], + "ReturnType": "System.Int64", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "134217728" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.HttpConnectionFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ConnectionId", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConnectionId", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LocalIpAddress", + "Parameters": [], + "ReturnType": "System.Net.IPAddress", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LocalIpAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.IPAddress" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LocalPort", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LocalPort", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemoteIpAddress", + "Parameters": [], + "ReturnType": "System.Net.IPAddress", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemoteIpAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.IPAddress" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemotePort", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemotePort", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.HttpRequestFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Protocol", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Protocol", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Scheme", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Scheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Method", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Method", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PathBase", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PathBase", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Path", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_QueryString", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_QueryString", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RawTarget", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RawTarget", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Headers", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Body", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Body", + "Parameters": [ + { + "Name": "value", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.HttpRequestIdentifierFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_TraceIdentifier", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TraceIdentifier", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.HttpRequestLifetimeFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequestAborted", + "Parameters": [], + "ReturnType": "System.Threading.CancellationToken", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestAborted", + "Parameters": [ + { + "Name": "value", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Abort", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.HttpResponseFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_StatusCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StatusCode", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ReasonPhrase", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ReasonPhrase", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Headers", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Body", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Body", + "Parameters": [ + { + "Name": "value", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasStarted", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStarting", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func<System.Object, System.Threading.Tasks.Task>" + }, + { + "Name": "state", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnCompleted", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func<System.Object, System.Threading.Tasks.Task>" + }, + { + "Name": "state", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.ItemsFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IItemsFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Items", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.Object, System.Object>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IItemsFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Items", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary<System.Object, System.Object>" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IItemsFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.QueryFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IQueryFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Query", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IQueryCollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IQueryFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Query", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IQueryCollection" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IQueryFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "query", + "Type": "Microsoft.AspNetCore.Http.IQueryCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.RequestCookiesFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IRequestCookiesFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Cookies", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IRequestCookieCollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IRequestCookiesFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Cookies", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IRequestCookieCollection" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IRequestCookiesFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "cookies", + "Type": "Microsoft.AspNetCore.Http.IRequestCookieCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.ResponseCookiesFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IResponseCookiesFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Cookies", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IResponseCookies", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IResponseCookiesFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + }, + { + "Name": "builderPool", + "Type": "Microsoft.Extensions.ObjectPool.ObjectPool<System.Text.StringBuilder>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.ServiceProvidersFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IServiceProvidersFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequestServices", + "Parameters": [], + "ReturnType": "System.IServiceProvider", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IServiceProvidersFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestServices", + "Parameters": [ + { + "Name": "value", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IServiceProvidersFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.TlsConnectionFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ClientCertificate", + "Parameters": [], + "ReturnType": "System.Security.Cryptography.X509Certificates.X509Certificate2", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClientCertificate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Cryptography.X509Certificates.X509Certificate2" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetClientCertificateAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Security.Cryptography.X509Certificates.X509Certificate2>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.HttpAuthenticationFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_User", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Handler", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.Authentication.IAuthenticationHandler", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Handler", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.IAuthenticationHandler" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.DependencyInjection.HttpServiceCollectionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddHttpContextAccessor", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.FormCollection+Enumerator", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MoveNext", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.IEnumerator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Current", + "Parameters": [], + "ReturnType": "System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>>", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HeaderDictionary+Enumerator", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MoveNext", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.IEnumerator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Current", + "Parameters": [], + "ReturnType": "System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<System.String, Microsoft.Extensions.Primitives.StringValues>>", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/Http/test/Authentication/DefaultAuthenticationManagerTests.cs b/src/Http/Http/test/Authentication/DefaultAuthenticationManagerTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..101f2b19eb4d22bdbb0c8369df20ecc837c6b5fd --- /dev/null +++ b/src/Http/Http/test/Authentication/DefaultAuthenticationManagerTests.cs @@ -0,0 +1,104 @@ +// 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. + +#pragma warning disable CS0618 // Type or member is obsolete +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Authentication.Internal +{ + public class DefaultAuthenticationManagerTests + { + + [Fact] + public async Task AuthenticateWithNoAuthMiddlewareThrows() + { + var context = CreateContext(); + await Assert.ThrowsAsync<InvalidOperationException>(async () => await context.Authentication.AuthenticateAsync("Foo")); + } + + [Theory] + [InlineData("Automatic")] + [InlineData("Foo")] + public async Task ChallengeWithNoAuthMiddlewareMayThrow(string scheme) + { + var context = CreateContext(); + await Assert.ThrowsAsync<InvalidOperationException>(() => context.Authentication.ChallengeAsync(scheme)); + } + + [Fact] + public async Task SignInWithNoAuthMiddlewareThrows() + { + var context = CreateContext(); + await Assert.ThrowsAsync<InvalidOperationException>(() => context.Authentication.SignInAsync("Foo", new ClaimsPrincipal())); + } + + [Fact] + public async Task SignOutWithNoAuthMiddlewareMayThrow() + { + var context = CreateContext(); + await Assert.ThrowsAsync<InvalidOperationException>(() => context.Authentication.SignOutAsync("Foo")); + } + + [Fact] + public async Task SignInOutIn() + { + var context = CreateContext(); + var handler = new AuthHandler(); + context.Features.Set<IHttpAuthenticationFeature>(new HttpAuthenticationFeature() { Handler = handler }); + var user = new ClaimsPrincipal(); + await context.Authentication.SignInAsync("ignored", user); + Assert.True(handler.SignedIn); + await context.Authentication.SignOutAsync("ignored"); + Assert.False(handler.SignedIn); + await context.Authentication.SignInAsync("ignored", user); + Assert.True(handler.SignedIn); + await context.Authentication.SignOutAsync("ignored", new AuthenticationProperties() { RedirectUri = "~/logout" }); + Assert.False(handler.SignedIn); + } + + private class AuthHandler : IAuthenticationHandler + { + public bool SignedIn { get; set; } + + public Task AuthenticateAsync(AuthenticateContext context) + { + throw new NotImplementedException(); + } + + public Task ChallengeAsync(ChallengeContext context) + { + throw new NotImplementedException(); + } + + public void GetDescriptions(DescribeSchemesContext context) + { + throw new NotImplementedException(); + } + + public Task SignInAsync(SignInContext context) + { + SignedIn = true; + context.Accept(); + return Task.FromResult(0); + } + + public Task SignOutAsync(SignOutContext context) + { + SignedIn = false; + context.Accept(); + return Task.FromResult(0); + } + } + + private HttpContext CreateContext() + { + var context = new DefaultHttpContext(); + return context; + } + } +} +#pragma warning restore CS0618 // Type or member is obsolete diff --git a/src/Http/Http/test/DefaultHttpContextTests.cs b/src/Http/Http/test/DefaultHttpContextTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..33f73cf191f11b0ee90896cbaf99b590ce8993f0 --- /dev/null +++ b/src/Http/Http/test/DefaultHttpContextTests.cs @@ -0,0 +1,352 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Reflection; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class DefaultHttpContextTests + { + [Fact] + public void GetOnSessionProperty_ThrowsOnMissingSessionFeature() + { + // Arrange + var context = new DefaultHttpContext(); + + // Act & Assert + var exception = Assert.Throws<InvalidOperationException>(() => context.Session); + Assert.Equal("Session has not been configured for this application or request.", exception.Message); + } + + [Fact] + public void GetOnSessionProperty_ReturnsAvailableSession() + { + // Arrange + var context = new DefaultHttpContext(); + var session = new TestSession(); + session.Set("key1", null); + session.Set("key2", null); + var feature = new BlahSessionFeature(); + feature.Session = session; + context.Features.Set<ISessionFeature>(feature); + + // Act & Assert + Assert.Same(session, context.Session); + context.Session.Set("key3", null); + Assert.Equal(3, context.Session.Keys.Count()); + } + + [Fact] + public void AllowsSettingSession_WithoutSettingUpSessionFeature_Upfront() + { + // Arrange + var session = new TestSession(); + var context = new DefaultHttpContext(); + + // Act + context.Session = session; + + // Assert + Assert.Same(session, context.Session); + } + + [Fact] + public void SettingSession_OverridesAvailableSession() + { + // Arrange + var context = new DefaultHttpContext(); + var session = new TestSession(); + session.Set("key1", null); + session.Set("key2", null); + var feature = new BlahSessionFeature(); + feature.Session = session; + context.Features.Set<ISessionFeature>(feature); + + // Act + context.Session = new TestSession(); + + // Assert + Assert.NotSame(session, context.Session); + Assert.Empty(context.Session.Keys); + } + + [Fact] + public void EmptyUserIsNeverNull() + { + var context = new DefaultHttpContext(new FeatureCollection()); + Assert.NotNull(context.User); + Assert.Single(context.User.Identities); + Assert.True(object.ReferenceEquals(context.User, context.User)); + Assert.False(context.User.Identity.IsAuthenticated); + Assert.True(string.IsNullOrEmpty(context.User.Identity.AuthenticationType)); + + context.User = null; + Assert.NotNull(context.User); + Assert.Single(context.User.Identities); + Assert.True(object.ReferenceEquals(context.User, context.User)); + Assert.False(context.User.Identity.IsAuthenticated); + Assert.True(string.IsNullOrEmpty(context.User.Identity.AuthenticationType)); + + context.User = new ClaimsPrincipal(); + Assert.NotNull(context.User); + Assert.Empty(context.User.Identities); + Assert.True(object.ReferenceEquals(context.User, context.User)); + Assert.Null(context.User.Identity); + + context.User = new ClaimsPrincipal(new ClaimsIdentity("SomeAuthType")); + Assert.Equal("SomeAuthType", context.User.Identity.AuthenticationType); + Assert.True(context.User.Identity.IsAuthenticated); + } + + [Fact] + public void GetItems_DefaultCollectionProvided() + { + var context = new DefaultHttpContext(new FeatureCollection()); + Assert.Null(context.Features.Get<IItemsFeature>()); + var items = context.Items; + Assert.NotNull(context.Features.Get<IItemsFeature>()); + Assert.NotNull(items); + Assert.Same(items, context.Items); + var item = new object(); + context.Items["foo"] = item; + Assert.Same(item, context.Items["foo"]); + } + + [Fact] + public void GetItems_DefaultRequestIdentifierAvailable() + { + var context = new DefaultHttpContext(new FeatureCollection()); + Assert.Null(context.Features.Get<IHttpRequestIdentifierFeature>()); + var traceIdentifier = context.TraceIdentifier; + Assert.NotNull(context.Features.Get<IHttpRequestIdentifierFeature>()); + Assert.NotNull(traceIdentifier); + Assert.Same(traceIdentifier, context.TraceIdentifier); + + context.TraceIdentifier = "Hello"; + Assert.Same("Hello", context.TraceIdentifier); + } + + [Fact] + public void SetItems_NewCollectionUsed() + { + var context = new DefaultHttpContext(new FeatureCollection()); + Assert.Null(context.Features.Get<IItemsFeature>()); + var items = new Dictionary<object, object>(); + context.Items = items; + Assert.NotNull(context.Features.Get<IItemsFeature>()); + Assert.Same(items, context.Items); + var item = new object(); + items["foo"] = item; + Assert.Same(item, context.Items["foo"]); + } + + [Fact] + public void UpdateFeatures_ClearsCachedFeatures() + { + var features = new FeatureCollection(); + features.Set<IHttpRequestFeature>(new HttpRequestFeature()); + features.Set<IHttpResponseFeature>(new HttpResponseFeature()); + features.Set<IHttpWebSocketFeature>(new TestHttpWebSocketFeature()); + + // featurecollection is set. all cached interfaces are null. + var context = new DefaultHttpContext(features); + TestAllCachedFeaturesAreNull(context, features); + Assert.Equal(3, features.Count()); + + // getting feature properties populates feature collection with defaults + TestAllCachedFeaturesAreSet(context, features); + Assert.NotEqual(3, features.Count()); + + // featurecollection is null. and all cached interfaces are null. + // only top level is tested because child objects are inaccessible. + context.Uninitialize(); + TestCachedFeaturesAreNull(context, null); + + + var newFeatures = new FeatureCollection(); + newFeatures.Set<IHttpRequestFeature>(new HttpRequestFeature()); + newFeatures.Set<IHttpResponseFeature>(new HttpResponseFeature()); + newFeatures.Set<IHttpWebSocketFeature>(new TestHttpWebSocketFeature()); + + // featurecollection is set to newFeatures. all cached interfaces are null. + context.Initialize(newFeatures); + TestAllCachedFeaturesAreNull(context, newFeatures); + Assert.Equal(3, newFeatures.Count()); + + // getting feature properties populates new feature collection with defaults + TestAllCachedFeaturesAreSet(context, newFeatures); + Assert.NotEqual(3, newFeatures.Count()); + } + + void TestAllCachedFeaturesAreNull(HttpContext context, IFeatureCollection features) + { + TestCachedFeaturesAreNull(context, features); + TestCachedFeaturesAreNull(context.Request, features); + TestCachedFeaturesAreNull(context.Response, features); +#pragma warning disable CS0618 // Type or member is obsolete + TestCachedFeaturesAreNull(context.Authentication, features); +#pragma warning restore CS0618 // Type or member is obsolete + TestCachedFeaturesAreNull(context.Connection, features); + TestCachedFeaturesAreNull(context.WebSockets, features); + } + + void TestCachedFeaturesAreNull(object value, IFeatureCollection features) + { + var type = value.GetType(); + + var field = type + .GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + .Single(f => + f.FieldType.GetTypeInfo().IsGenericType && + f.FieldType.GetGenericTypeDefinition() == typeof(FeatureReferences<>)); + + var boxedExpectedStruct = features == null ? + Activator.CreateInstance(field.FieldType) : + Activator.CreateInstance(field.FieldType, features); + + var boxedActualStruct = field.GetValue(value); + + Assert.Equal(boxedExpectedStruct, boxedActualStruct); + } + + void TestAllCachedFeaturesAreSet(HttpContext context, IFeatureCollection features) + { + TestCachedFeaturesAreSet(context, features); + TestCachedFeaturesAreSet(context.Request, features); + TestCachedFeaturesAreSet(context.Response, features); +#pragma warning disable CS0618 // Type or member is obsolete + TestCachedFeaturesAreSet(context.Authentication, features); +#pragma warning restore CS0618 // Type or member is obsolete + TestCachedFeaturesAreSet(context.Connection, features); + TestCachedFeaturesAreSet(context.WebSockets, features); + } + + void TestCachedFeaturesAreSet(object value, IFeatureCollection features) + { + var type = value.GetType(); + + var properties = type + .GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.PropertyType.GetTypeInfo().IsInterface); + + TestFeatureProperties(value, features, properties); + + var fields = type + .GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(f => f.FieldType.GetTypeInfo().IsInterface); + + foreach (var field in fields) + { + if (field.FieldType == typeof(IFeatureCollection)) + { + Assert.Same(features, field.GetValue(value)); + } + else + { + var v = field.GetValue(value); + Assert.Same(features[field.FieldType], v); + Assert.NotNull(v); + } + } + + } + + private static void TestFeatureProperties(object value, IFeatureCollection features, IEnumerable<PropertyInfo> properties) + { + foreach (var property in properties) + { + if (property.PropertyType == typeof(IFeatureCollection)) + { + Assert.Same(features, property.GetValue(value)); + } + else + { + if (property.Name.Contains("Feature")) + { + var v = property.GetValue(value); + Assert.Same(features[property.PropertyType], v); + Assert.NotNull(v); + } + } + } + } + + private HttpContext CreateContext() + { + var context = new DefaultHttpContext(); + return context; + } + + private class TestSession : ISession + { + private Dictionary<string, byte[]> _store + = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase); + + public string Id { get; set; } + + public bool IsAvailable { get; } = true; + + public IEnumerable<string> Keys { get { return _store.Keys; } } + + public void Clear() + { + _store.Clear(); + } + + public Task CommitAsync(CancellationToken cancellationToken) + { + return Task.FromResult(0); + } + + public Task LoadAsync(CancellationToken cancellationToken) + { + return Task.FromResult(0); + } + + public void Remove(string key) + { + _store.Remove(key); + } + + public void Set(string key, byte[] value) + { + _store[key] = value; + } + + public bool TryGetValue(string key, out byte[] value) + { + return _store.TryGetValue(key, out value); + } + } + + private class BlahSessionFeature : ISessionFeature + { + public ISession Session { get; set; } + } + + private class TestHttpWebSocketFeature : IHttpWebSocketFeature + { + public bool IsWebSocketRequest + { + get + { + throw new NotImplementedException(); + } + } + + public Task<WebSocket> AcceptAsync(WebSocketAcceptContext context) + { + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http/test/Features/FakeResponseFeature.cs b/src/Http/Http/test/Features/FakeResponseFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..43a7acab583f921f030032ee250c8cf49bae0c73 --- /dev/null +++ b/src/Http/Http/test/Features/FakeResponseFeature.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class FakeResponseFeature : HttpResponseFeature + { + List<Tuple<Func<object, Task>, object>> _onCompletedCallbacks = new List<Tuple<Func<object, Task>, object>>(); + + public override void OnCompleted(Func<object, Task> callback, object state) + { + _onCompletedCallbacks.Add(new Tuple<Func<object, Task>, object>(callback, state)); + } + + public async Task CompleteAsync() + { + var callbacks = _onCompletedCallbacks; + _onCompletedCallbacks = null; + foreach (var callback in callbacks) + { + await callback.Item1(callback.Item2); + } + } + } +} diff --git a/src/Http/Http/test/Features/FormFeatureTests.cs b/src/Http/Http/test/Features/FormFeatureTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..591f46a43ec9b4364d3d47c5eeff8aa83cba5e42 --- /dev/null +++ b/src/Http/Http/test/Features/FormFeatureTests.cs @@ -0,0 +1,521 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class FormFeatureTests + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_SimpleData_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes("foo=bar&baz=2"); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set<IHttpResponseFeature>(responseFeature); + context.Request.ContentType = "application/x-www-form-urlencoded; charset=utf-8"; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set<IFormFeature>(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.Equal("bar", formCollection["foo"]); + Assert.Equal("2", formCollection["baz"]); + Assert.Equal(bufferRequest, context.Request.Body.CanSeek); + if (bufferRequest) + { + Assert.Equal(0, context.Request.Body.Position); + } + + // Cached + formFeature = context.Features.Get<IFormFeature>(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + + // Cleanup + await responseFeature.CompleteAsync(); + } + + private const string MultipartContentType = "multipart/form-data; boundary=WebKitFormBoundary5pDRpGheQXaM8k3T"; + + private const string MultipartContentTypeWithSpecialCharacters = "multipart/form-data; boundary=\"WebKitFormBoundary/:5pDRpGheQXaM8k3T\""; + + private const string EmptyMultipartForm = "--WebKitFormBoundary5pDRpGheQXaM8k3T--"; + + // Note that CRLF (\r\n) is required. You can't use multi-line C# strings here because the line breaks on Linux are just LF. + private const string MultipartFormEnd = "--WebKitFormBoundary5pDRpGheQXaM8k3T--\r\n"; + + private const string MultipartFormEndWithSpecialCharacters = "--WebKitFormBoundary/:5pDRpGheQXaM8k3T--\r\n"; + + private const string MultipartFormField = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + +"Content-Disposition: form-data; name=\"description\"\r\n" + +"\r\n" + +"Foo\r\n"; + + private const string MultipartFormFile = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + +"Content-Disposition: form-data; name=\"myfile1\"; filename=\"temp.html\"\r\n" + +"Content-Type: text/html\r\n" + +"\r\n" + +"<html><body>Hello World</body></html>\r\n"; + + private const string MultipartFormEncodedFilename = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + +"Content-Disposition: form-data; name=\"myfile1\"; filename=\"temp.html\"; filename*=utf-8\'\'t%c3%a9mp.html\r\n" + +"Content-Type: text/html\r\n" + +"\r\n" + +"<html><body>Hello World</body></html>\r\n"; + + private const string MultipartFormFileSpecialCharacters = "--WebKitFormBoundary/:5pDRpGheQXaM8k3T\r\n" + +"Content-Disposition: form-data; name=\"description\"\r\n" + +"\r\n" + +"Foo\r\n"; + + + private const string MultipartFormWithField = + MultipartFormField + + MultipartFormEnd; + + private const string MultipartFormWithFile = + MultipartFormFile + + MultipartFormEnd; + + private const string MultipartFormWithFieldAndFile = + MultipartFormField + + MultipartFormFile + + MultipartFormEnd; + + private const string MultipartFormWithEncodedFilename = + MultipartFormEncodedFilename + + MultipartFormEnd; + + private const string MultipartFormWithSpecialCharacters = + MultipartFormFileSpecialCharacters + + MultipartFormEndWithSpecialCharacters; + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadForm_EmptyMultipart_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(EmptyMultipartForm); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set<IHttpResponseFeature>(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set<IFormFeature>(formFeature); + + var formCollection = context.Request.Form; + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get<IFormFeature>(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formCollection, formFeature.Form); + Assert.Same(formCollection, await context.Request.ReadFormAsync()); + + // Content + Assert.Equal(0, formCollection.Count); + Assert.NotNull(formCollection.Files); + Assert.Equal(0, formCollection.Files.Count); + + // Cleanup + await responseFeature.CompleteAsync(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadForm_MultipartWithField_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithField); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set<IHttpResponseFeature>(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set<IFormFeature>(formFeature); + + var formCollection = context.Request.Form; + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get<IFormFeature>(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formCollection, formFeature.Form); + Assert.Same(formCollection, await context.Request.ReadFormAsync()); + + // Content + Assert.Equal(1, formCollection.Count); + Assert.Equal("Foo", formCollection["description"]); + + Assert.NotNull(formCollection.Files); + Assert.Equal(0, formCollection.Files.Count); + + // Cleanup + await responseFeature.CompleteAsync(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_MultipartWithFile_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFile); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set<IHttpResponseFeature>(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set<IFormFeature>(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get<IFormFeature>(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + Assert.Same(formCollection, context.Request.Form); + + // Content + Assert.Equal(0, formCollection.Count); + + Assert.NotNull(formCollection.Files); + Assert.Equal(1, formCollection.Files.Count); + + var file = formCollection.Files["myfile1"]; + Assert.Equal("myfile1", file.Name); + Assert.Equal("temp.html", file.FileName); + Assert.Equal("text/html", file.ContentType); + Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition); + var body = file.OpenReadStream(); + using (var reader = new StreamReader(body)) + { + Assert.True(body.CanSeek); + var content = reader.ReadToEnd(); + Assert.Equal("<html><body>Hello World</body></html>", content); + } + + await responseFeature.CompleteAsync(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_MultipartWithFileAndQuotedBoundaryString_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithSpecialCharacters); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set<IHttpResponseFeature>(responseFeature); + context.Request.ContentType = MultipartContentTypeWithSpecialCharacters; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set<IFormFeature>(formFeature); + + var formCollection = context.Request.Form; + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get<IFormFeature>(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formCollection, formFeature.Form); + Assert.Same(formCollection, await context.Request.ReadFormAsync()); + + // Content + Assert.Equal(1, formCollection.Count); + Assert.Equal("Foo", formCollection["description"]); + + Assert.NotNull(formCollection.Files); + Assert.Equal(0, formCollection.Files.Count); + + // Cleanup + await responseFeature.CompleteAsync(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_MultipartWithEncodedFilename_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithEncodedFilename); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set<IHttpResponseFeature>(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set<IFormFeature>(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get<IFormFeature>(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + Assert.Same(formCollection, context.Request.Form); + + // Content + Assert.Equal(0, formCollection.Count); + + Assert.NotNull(formCollection.Files); + Assert.Equal(1, formCollection.Files.Count); + + var file = formCollection.Files["myfile1"]; + Assert.Equal("myfile1", file.Name); + Assert.Equal("t\u00e9mp.html", file.FileName); + Assert.Equal("text/html", file.ContentType); + Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""; filename*=utf-8''t%c3%a9mp.html", file.ContentDisposition); + var body = file.OpenReadStream(); + using (var reader = new StreamReader(body)) + { + Assert.True(body.CanSeek); + var content = reader.ReadToEnd(); + Assert.Equal("<html><body>Hello World</body></html>", content); + } + + await responseFeature.CompleteAsync(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_MultipartWithFieldAndFile_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFieldAndFile); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set<IHttpResponseFeature>(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set<IFormFeature>(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get<IFormFeature>(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + Assert.Same(formCollection, context.Request.Form); + + // Content + Assert.Equal(1, formCollection.Count); + Assert.Equal("Foo", formCollection["description"]); + + Assert.NotNull(formCollection.Files); + Assert.Equal(1, formCollection.Files.Count); + + var file = formCollection.Files["myfile1"]; + Assert.Equal("text/html", file.ContentType); + Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition); + var body = file.OpenReadStream(); + using (var reader = new StreamReader(body)) + { + Assert.True(body.CanSeek); + var content = reader.ReadToEnd(); + Assert.Equal("<html><body>Hello World</body></html>", content); + } + + await responseFeature.CompleteAsync(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitExceeded_Throw(bool bufferRequest) + { + var formContent = new List<byte>(); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormField)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormField)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormField)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormEnd)); + + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set<IHttpResponseFeature>(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent.ToArray()); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest, ValueCountLimit = 2 }); + context.Features.Set<IFormFeature>(formFeature); + + var exception = await Assert.ThrowsAsync<InvalidDataException> (() => context.Request.ReadFormAsync()); + Assert.Equal("Form value count limit 2 exceeded.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitExceededWithFiles_Throw(bool bufferRequest) + { + var formContent = new List<byte>(); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFile)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFile)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFile)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormEnd)); + + + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set<IHttpResponseFeature>(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent.ToArray()); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest, ValueCountLimit = 2 }); + context.Features.Set<IFormFeature>(formFeature); + + var exception = await Assert.ThrowsAsync<InvalidDataException> (() => context.Request.ReadFormAsync()); + Assert.Equal("Form value count limit 2 exceeded.", exception.Message); + } + + [Theory] + // FileBufferingReadStream transitions to disk storage after 30kb, and stops pooling buffers at 1mb. + [InlineData(true, 1024)] + [InlineData(false, 1024)] + [InlineData(true, 40 * 1024)] + [InlineData(false, 40 * 1024)] + [InlineData(true, 4 * 1024 * 1024)] + [InlineData(false, 4 * 1024 * 1024)] + public async Task ReadFormAsync_MultipartWithFieldAndMediumFile_ReturnsParsedFormCollection(bool bufferRequest, int fileSize) + { + var fileContents = CreateFile(fileSize); + var formContent = CreateMultipartWithFormAndFile(fileContents); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set<IHttpResponseFeature>(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set<IFormFeature>(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get<IFormFeature>(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + Assert.Same(formCollection, context.Request.Form); + + // Content + Assert.Equal(1, formCollection.Count); + Assert.Equal("Foo", formCollection["description"]); + + Assert.NotNull(formCollection.Files); + Assert.Equal(1, formCollection.Files.Count); + + var file = formCollection.Files["myfile1"]; + Assert.Equal("text/html", file.ContentType); + Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition); + using (var body = file.OpenReadStream()) + { + Assert.True(body.CanSeek); + CompareStreams(fileContents, body); + } + + await responseFeature.CompleteAsync(); + } + + private Stream CreateFile(int size) + { + var stream = new MemoryStream(size); + var bytes = Encoding.ASCII.GetBytes("HelloWorld_ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz,0123456789;"); + int written = 0; + while (written < size) + { + var toWrite = Math.Min(size - written, bytes.Length); + stream.Write(bytes, 0, toWrite); + written += toWrite; + } + stream.Position = 0; + return stream; + } + + private Stream CreateMultipartWithFormAndFile(Stream fileContents) + { + var stream = new MemoryStream(); + var header = +MultipartFormField + +"--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + +"Content-Disposition: form-data; name=\"myfile1\"; filename=\"temp.html\"\r\n" + +"Content-Type: text/html\r\n" + +"\r\n"; + var footer = +"\r\n--WebKitFormBoundary5pDRpGheQXaM8k3T--"; + + var bytes = Encoding.ASCII.GetBytes(header); + stream.Write(bytes, 0, bytes.Length); + + fileContents.CopyTo(stream); + fileContents.Position = 0; + + bytes = Encoding.ASCII.GetBytes(footer); + stream.Write(bytes, 0, bytes.Length); + stream.Position = 0; + return stream; + } + + private void CompareStreams(Stream streamA, Stream streamB) + { + Assert.Equal(streamA.Length, streamB.Length); + byte[] bytesA = new byte[1024], bytesB = new byte[1024]; + var readA = streamA.Read(bytesA, 0, bytesA.Length); + var readB = streamB.Read(bytesB, 0, bytesB.Length); + Assert.Equal(readA, readB); + var loops = 0; + while (readA > 0) + { + for (int i = 0; i < readA; i++) + { + if (bytesA[i] != bytesB[i]) + { + throw new Exception($"Value mismatch at loop {loops}, index {i}; A:{bytesA[i]}, B:{bytesB[i]}"); + } + } + + readA = streamA.Read(bytesA, 0, bytesA.Length); + readB = streamB.Read(bytesB, 0, bytesB.Length); + Assert.Equal(readA, readB); + loops++; + } + } + } +} diff --git a/src/Http/Http/test/Features/HttpRequestIdentifierFeatureTests.cs b/src/Http/Http/test/Features/HttpRequestIdentifierFeatureTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..7b17028cdfdfbf28a29164f541cab7f127d465a7 --- /dev/null +++ b/src/Http/Http/test/Features/HttpRequestIdentifierFeatureTests.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class HttpRequestIdentifierFeatureTests + { + [Fact] + public void TraceIdentifier_ReturnsId() + { + var feature = new HttpRequestIdentifierFeature(); + + var id = feature.TraceIdentifier; + + Assert.NotNull(id); + } + + [Fact] + public void TraceIdentifier_ReturnsStableId() + { + var feature = new HttpRequestIdentifierFeature(); + + var id1 = feature.TraceIdentifier; + var id2 = feature.TraceIdentifier; + + Assert.Equal(id1, id2); + } + + [Fact] + public void TraceIdentifier_ReturnsUniqueIdForDifferentInstances() + { + var feature1 = new HttpRequestIdentifierFeature(); + var feature2 = new HttpRequestIdentifierFeature(); + + var id1 = feature1.TraceIdentifier; + var id2 = feature2.TraceIdentifier; + + Assert.NotEqual(id1, id2); + } + } +} \ No newline at end of file diff --git a/src/Http/Http/test/Features/NonSeekableReadStream.cs b/src/Http/Http/test/Features/NonSeekableReadStream.cs new file mode 100644 index 0000000000000000000000000000000000000000..279da7992b94f8d66312211130af1680cc5b4d19 --- /dev/null +++ b/src/Http/Http/test/Features/NonSeekableReadStream.cs @@ -0,0 +1,72 @@ +// 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.Http.Features +{ + public class NonSeekableReadStream : Stream + { + private Stream _inner; + + public NonSeekableReadStream(byte[] data) + : this(new MemoryStream(data)) + { + } + + public NonSeekableReadStream(Stream inner) + { + _inner = inner; + } + + public override bool CanRead => _inner.CanRead; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + 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) + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _inner.Read(buffer, offset, count); + } + + public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.ReadAsync(buffer, offset, count, cancellationToken); + } + } +} diff --git a/src/Http/Http/test/Features/QueryFeatureTests.cs b/src/Http/Http/test/Features/QueryFeatureTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..e43e3ce7a976bdddcf5aad9a419a13792ff7a9f1 --- /dev/null +++ b/src/Http/Http/test/Features/QueryFeatureTests.cs @@ -0,0 +1,67 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class QueryFeatureTests + { + [Fact] + public void QueryReturnsParsedQueryCollection() + { + // Arrange + var features = new FeatureCollection(); + var request = new HttpRequestFeature(); + request.QueryString = "foo=bar"; + features[typeof(IHttpRequestFeature)] = request; + + var provider = new QueryFeature(features); + + // Act + var queryCollection = provider.Query; + + // Assert + Assert.Equal("bar", queryCollection["foo"]); + } + + [Theory] + [InlineData("?q", "q")] + [InlineData("?q&", "q")] + [InlineData("?q1=abc&q2", "q2")] + [InlineData("?q=", "q")] + [InlineData("?q=&", "q")] + public void KeyWithoutValuesAddedToQueryCollection(string queryString, string emptyParam) + { + var features = new FeatureCollection(); + var request = new HttpRequestFeature(); + request.QueryString = queryString; + features[typeof(IHttpRequestFeature)] = request; + + var provider = new QueryFeature(features); + + var queryCollection = provider.Query; + + Assert.True(queryCollection.Keys.Contains(emptyParam)); + Assert.Equal(string.Empty, queryCollection[emptyParam]); + } + + [Theory] + [InlineData("?&&")] + [InlineData("?&")] + [InlineData("&&")] + public void EmptyKeysNotAddedToQueryCollection(string queryString) + { + var features = new FeatureCollection(); + var request = new HttpRequestFeature(); + request.QueryString = queryString; + features[typeof(IHttpRequestFeature)] = request; + + var provider = new QueryFeature(features); + + var queryCollection = provider.Query; + + Assert.Equal(0, queryCollection.Count); + } + } +} diff --git a/src/Http/Http/test/HeaderDictionaryTests.cs b/src/Http/Http/test/HeaderDictionaryTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..03d642a0181dc52d8b03023c49dd2a38b025d3ca --- /dev/null +++ b/src/Http/Http/test/HeaderDictionaryTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class HeaderDictionaryTests + { + public static TheoryData HeaderSegmentData => new TheoryData<IEnumerable<string>> + { + new[] { "Value1", "Value2", "Value3", "Value4" }, + new[] { "Value1", "", "Value3", "Value4" }, + new[] { "Value1", "", "", "Value4" }, + new[] { "Value1", "", null, "Value4" }, + new[] { "", "", "", "" }, + new[] { "", null, "", null }, + }; + + [Fact] + public void PropertiesAreAccessible() + { + var headers = new HeaderDictionary( + new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase) + { + { "Header1", "Value1" } + }); + + Assert.Single(headers); + Assert.Equal<string>(new[] { "Header1" }, headers.Keys); + Assert.True(headers.ContainsKey("header1")); + Assert.False(headers.ContainsKey("header2")); + Assert.Equal("Value1", headers["header1"]); + Assert.Equal(new[] { "Value1" }, headers["header1"].ToArray()); + } + + [Theory] + [MemberData(nameof(HeaderSegmentData))] + public void EmptyHeaderSegmentsAreIgnored(IEnumerable<string> segments) + { + var header = string.Join(",", segments); + + var headers = new HeaderDictionary( + new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase) + { + { "Header1", header}, + }); + + var result = headers.GetCommaSeparatedValues("Header1"); + var expectedResult = segments.Where(s => !string.IsNullOrEmpty(s)); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void EmtpyQuotedHeaderSegmentsAreIgnored() + { + var headers = new HeaderDictionary( + new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase) + { + { "Header1", "Value1,\"\",,Value2" }, + }); + + var result = headers.GetCommaSeparatedValues("Header1"); + Assert.Equal(new[] { "Value1", "Value2" }, result); + } + + [Fact] + public void ReadActionsWorkWhenReadOnly() + { + var headers = new HeaderDictionary( + new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase) + { + { "Header1", "Value1" } + }); + + headers.IsReadOnly = true; + + Assert.Single(headers); + Assert.Equal<string>(new[] { "Header1" }, headers.Keys); + Assert.True(headers.ContainsKey("header1")); + Assert.False(headers.ContainsKey("header2")); + Assert.Equal("Value1", headers["header1"]); + Assert.Equal(new[] { "Value1" }, headers["header1"].ToArray()); + } + + [Fact] + public void WriteActionsThrowWhenReadOnly() + { + var headers = new HeaderDictionary(); + headers.IsReadOnly = true; + + Assert.Throws<InvalidOperationException>(() => headers["header1"] = "value1"); + Assert.Throws<InvalidOperationException>(() => ((IDictionary<string, StringValues>)headers)["header1"] = "value1"); + Assert.Throws<InvalidOperationException>(() => headers.ContentLength = 12); + Assert.Throws<InvalidOperationException>(() => headers.Add(new KeyValuePair<string, StringValues>("header1", "value1"))); + Assert.Throws<InvalidOperationException>(() => headers.Add("header1", "value1")); + Assert.Throws<InvalidOperationException>(() => headers.Clear()); + Assert.Throws<InvalidOperationException>(() => headers.Remove(new KeyValuePair<string, StringValues>("header1", "value1"))); + Assert.Throws<InvalidOperationException>(() => headers.Remove("header1")); + } + } +} \ No newline at end of file diff --git a/src/Http/Http/test/HttpContextFactoryTests.cs b/src/Http/Http/test/HttpContextFactoryTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..ba983198e75eccbff1f091cc5df3f4c8e5fbe150 --- /dev/null +++ b/src/Http/Http/test/HttpContextFactoryTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class HttpContextFactoryTests + { + [Fact] + public void CreateHttpContextSetsHttpContextAccessor() + { + // Arrange + var accessor = new HttpContextAccessor(); + var contextFactory = new HttpContextFactory(Options.Create(new FormOptions()), accessor); + + // Act + var context = contextFactory.Create(new FeatureCollection()); + + // Assert + Assert.True(ReferenceEquals(context, accessor.HttpContext)); + } + + [Fact] + public void AllowsCreatingContextWithoutSettingAccessor() + { + // Arrange + var contextFactory = new HttpContextFactory(Options.Create(new FormOptions())); + + // Act & Assert + var context = contextFactory.Create(new FeatureCollection()); + contextFactory.Dispose(context); + } + } +} \ No newline at end of file diff --git a/src/Http/Http/test/HttpServiceCollectionExtensionsTests.cs b/src/Http/Http/test/HttpServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..a317e993460b72cb631a68be5a536179febb71ea --- /dev/null +++ b/src/Http/Http/test/HttpServiceCollectionExtensionsTests.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class HttpServiceCollectionExtensionsTests + { + [Fact] + public void AddHttpContextAccessor_AddsWithCorrectLifetime() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddHttpContextAccessor(); + + // Assert + var descriptor = services[0]; + Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); + Assert.Equal(typeof(HttpContextAccessor), descriptor.ImplementationType); + } + + [Fact] + public void AddHttpContextAccessor_ThrowsWithoutServices() + { + Assert.Throws<ArgumentNullException>("services", () => HttpServiceCollectionExtensions.AddHttpContextAccessor(null)); + } + } +} diff --git a/src/Http/Http/test/Internal/ApplicationBuilderTests.cs b/src/Http/Http/test/Internal/ApplicationBuilderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..e1336c82ba09e5198aa1f6fed740e73531a453c9 --- /dev/null +++ b/src/Http/Http/test/Internal/ApplicationBuilderTests.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Builder.Internal +{ + public class ApplicationBuilderTests + { + [Fact] + public void BuildReturnsCallableDelegate() + { + var builder = new ApplicationBuilder(null); + var app = builder.Build(); + + var httpContext = new DefaultHttpContext(); + + app.Invoke(httpContext); + Assert.Equal(404, httpContext.Response.StatusCode); + } + + [Fact] + public void PropertiesDictionaryIsDistinctAfterNew() + { + var builder1 = new ApplicationBuilder(null); + builder1.Properties["test"] = "value1"; + + var builder2 = builder1.New(); + builder2.Properties["test"] = "value2"; + + Assert.Equal("value1", builder1.Properties["test"]); + } + } +} \ No newline at end of file diff --git a/src/Http/Http/test/Internal/BindingAddressTests.cs b/src/Http/Http/test/Internal/BindingAddressTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..3bca310e8239931a766c2eb52fa815d5731042f8 --- /dev/null +++ b/src/Http/Http/test/Internal/BindingAddressTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; +namespace Microsoft.AspNetCore.Http.Internal.Tests +{ + public class BindingAddressTests + { + [Theory] + [InlineData("")] + [InlineData("5000")] + [InlineData("//noscheme")] + public void FromUriThrowsForUrlsWithoutSchemeDelimiter(string url) + { + Assert.Throws<FormatException>(() => BindingAddress.Parse(url)); + } + + [Theory] + [InlineData("://")] + [InlineData("://:5000")] + [InlineData("http://")] + [InlineData("http://:5000")] + [InlineData("http:///")] + [InlineData("http:///:5000")] + [InlineData("http:////")] + [InlineData("http:////:5000")] + public void FromUriThrowsForUrlsWithoutHost(string url) + { + Assert.Throws<FormatException>(() => BindingAddress.Parse(url)); + } + + [Theory] + [InlineData("://emptyscheme", "", "emptyscheme", 0, "", "://emptyscheme:0")] + [InlineData("http://+", "http", "+", 80, "", "http://+:80")] + [InlineData("http://*", "http", "*", 80, "", "http://*:80")] + [InlineData("http://localhost", "http", "localhost", 80, "", "http://localhost:80")] + [InlineData("http://www.example.com", "http", "www.example.com", 80, "", "http://www.example.com:80")] + [InlineData("https://www.example.com", "https", "www.example.com", 443, "", "https://www.example.com:443")] + [InlineData("http://www.example.com/", "http", "www.example.com", 80, "", "http://www.example.com:80")] + [InlineData("http://www.example.com/foo?bar=baz", "http", "www.example.com", 80, "/foo?bar=baz", "http://www.example.com:80/foo?bar=baz")] + [InlineData("http://www.example.com:5000", "http", "www.example.com", 5000, "", null)] + [InlineData("https://www.example.com:5000", "https", "www.example.com", 5000, "", null)] + [InlineData("http://www.example.com:5000/", "http", "www.example.com", 5000, "", "http://www.example.com:5000")] + [InlineData("http://www.example.com:NOTAPORT", "http", "www.example.com:NOTAPORT", 80, "", "http://www.example.com:notaport:80")] + [InlineData("https://www.example.com:NOTAPORT", "https", "www.example.com:NOTAPORT", 443, "", "https://www.example.com:notaport:443")] + [InlineData("http://www.example.com:NOTAPORT/", "http", "www.example.com:NOTAPORT", 80, "", "http://www.example.com:notaport:80")] + [InlineData("http://foo:/tmp/kestrel-test.sock:5000/doesn't/matter", "http", "foo:", 80, "/tmp/kestrel-test.sock:5000/doesn't/matter", "http://foo::80/tmp/kestrel-test.sock:5000/doesn't/matter")] + [InlineData("http://unix:foo/tmp/kestrel-test.sock", "http", "unix:foo", 80, "/tmp/kestrel-test.sock", "http://unix:foo:80/tmp/kestrel-test.sock")] + [InlineData("http://unix:5000/tmp/kestrel-test.sock", "http", "unix", 5000, "/tmp/kestrel-test.sock", "http://unix:5000/tmp/kestrel-test.sock")] + [InlineData("http://unix:/tmp/kestrel-test.sock", "http", "unix:/tmp/kestrel-test.sock", 0, "", null)] + [InlineData("https://unix:/tmp/kestrel-test.sock", "https", "unix:/tmp/kestrel-test.sock", 0, "", null)] + [InlineData("http://unix:/tmp/kestrel-test.sock:", "http", "unix:/tmp/kestrel-test.sock", 0, "", "http://unix:/tmp/kestrel-test.sock")] + [InlineData("http://unix:/tmp/kestrel-test.sock:/", "http", "unix:/tmp/kestrel-test.sock", 0, "", "http://unix:/tmp/kestrel-test.sock")] + [InlineData("http://unix:/tmp/kestrel-test.sock:5000/doesn't/matter", "http", "unix:/tmp/kestrel-test.sock", 0, "5000/doesn't/matter", "http://unix:/tmp/kestrel-test.sock")] + public void UrlsAreParsedCorrectly(string url, string scheme, string host, int port, string pathBase, string toString) + { + var serverAddress = BindingAddress.Parse(url); + + Assert.Equal(scheme, serverAddress.Scheme); + Assert.Equal(host, serverAddress.Host); + Assert.Equal(port, serverAddress.Port); + Assert.Equal(pathBase, serverAddress.PathBase); + + Assert.Equal(toString ?? url, serverAddress.ToString()); + } + } +} diff --git a/src/Http/Http/test/Internal/BufferingHelperTests.cs b/src/Http/Http/test/Internal/BufferingHelperTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..9ad48986f501ac56384f76e93fb3cbaa0fc64ac6 --- /dev/null +++ b/src/Http/Http/test/Internal/BufferingHelperTests.cs @@ -0,0 +1,19 @@ +// 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.IO; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class BufferingHelperTests + { + [Fact] + public void GetTempDirectory_Returns_Valid_Location() + { + var tempDirectory = BufferingHelper.TempDirectory; + Assert.NotNull(tempDirectory); + Assert.True(Directory.Exists(tempDirectory)); + } + } +} \ No newline at end of file diff --git a/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs b/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..dbe1d54dd0844ebd3c25bde45b688ec19d4e4943 --- /dev/null +++ b/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs @@ -0,0 +1,235 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class DefaultHttpRequestTests + { + [Theory] + [InlineData(0)] + [InlineData(9001)] + [InlineData(65535)] + public void GetContentLength_ReturnsParsedHeader(long value) + { + // Arrange + var request = GetRequestWithContentLength(value.ToString(CultureInfo.InvariantCulture)); + + // Act and Assert + Assert.Equal(value, request.ContentLength); + } + + [Fact] + public void GetContentLength_ReturnsNullIfHeaderDoesNotExist() + { + // Arrange + var request = GetRequestWithContentLength(contentLength: null); + + // Act and Assert + Assert.Null(request.ContentLength); + } + + [Theory] + [InlineData("cant-parse-this")] + [InlineData("-1000")] + [InlineData("1000.00")] + [InlineData("100/5")] + public void GetContentLength_ReturnsNullIfHeaderCannotBeParsed(string contentLength) + { + // Arrange + var request = GetRequestWithContentLength(contentLength); + + // Act and Assert + Assert.Null(request.ContentLength); + } + + [Fact] + public void GetContentType_ReturnsNullIfHeaderDoesNotExist() + { + // Arrange + var request = GetRequestWithContentType(contentType: null); + + // Act and Assert + Assert.Null(request.ContentType); + } + + [Fact] + public void Host_GetsHostFromHeaders() + { + // Arrange + const string expected = "localhost:9001"; + + var headers = new HeaderDictionary() + { + { "Host", expected }, + }; + + var request = CreateRequest(headers); + + // Act + var host = request.Host; + + // Assert + Assert.Equal(expected, host.Value); + } + + [Fact] + public void Host_DecodesPunyCode() + { + // Arrange + const string expected = "löcalhöst"; + + var headers = new HeaderDictionary() + { + { "Host", "xn--lcalhst-90ae" }, + }; + + var request = CreateRequest(headers); + + // Act + var host = request.Host; + + // Assert + Assert.Equal(expected, host.Value); + } + + [Fact] + public void Host_EncodesPunyCode() + { + // Arrange + const string expected = "xn--lcalhst-90ae"; + + var headers = new HeaderDictionary(); + + var request = CreateRequest(headers); + + // Act + request.Host = new HostString("löcalhöst"); + + // Assert + Assert.Equal(expected, headers["Host"][0]); + } + + [Fact] + public void IsHttps_CorrectlyReflectsScheme() + { + var request = new DefaultHttpContext().Request; + Assert.Equal(string.Empty, request.Scheme); + Assert.False(request.IsHttps); + request.IsHttps = true; + Assert.Equal("https", request.Scheme); + request.IsHttps = false; + Assert.Equal("http", request.Scheme); + request.Scheme = "ftp"; + Assert.False(request.IsHttps); + request.Scheme = "HTTPS"; + Assert.True(request.IsHttps); + } + + [Fact] + public void Query_GetAndSet() + { + var request = new DefaultHttpContext().Request; + var requestFeature = request.HttpContext.Features.Get<IHttpRequestFeature>(); + Assert.Equal(string.Empty, requestFeature.QueryString); + Assert.Equal(QueryString.Empty, request.QueryString); + var query0 = request.Query; + Assert.NotNull(query0); + Assert.Equal(0, query0.Count); + + requestFeature.QueryString = "?name0=value0&name1=value1"; + var query1 = request.Query; + Assert.NotSame(query0, query1); + Assert.Equal(2, query1.Count); + Assert.Equal("value0", query1["name0"]); + Assert.Equal("value1", query1["name1"]); + + var query2 = new QueryCollection( new Dictionary<string, StringValues>() + { + { "name2", "value2" } + }); + + request.Query = query2; + Assert.Same(query2, request.Query); + Assert.Equal("?name2=value2", requestFeature.QueryString); + Assert.Equal(new QueryString("?name2=value2"), request.QueryString); + } + + [Fact] + public void Cookies_GetAndSet() + { + var request = new DefaultHttpContext().Request; + var cookieHeaders = request.Headers["Cookie"]; + Assert.Empty(cookieHeaders); + var cookies0 = request.Cookies; + Assert.Empty(cookies0); + Assert.Null(cookies0["key0"]); + Assert.False(cookies0.ContainsKey("key0")); + + var newCookies = new[] { "name0=value0%2C", "%5Ename1=value1" }; + request.Headers["Cookie"] = newCookies; + + cookies0 = RequestCookieCollection.Parse(newCookies); + var cookies1 = request.Cookies; + Assert.Equal(cookies0, cookies1); + Assert.Equal(2, cookies1.Count); + Assert.Equal("value0,", cookies1["name0"]); + Assert.Equal("value1", cookies1["^name1"]); + Assert.Equal(newCookies, request.Headers["Cookie"]); + + var cookies2 = new RequestCookieCollection(new Dictionary<string,string>() + { + { "name2", "value2" } + }); + request.Cookies = cookies2; + Assert.Equal(cookies2, request.Cookies); + Assert.Equal("value2", request.Cookies["name2"]); + cookieHeaders = request.Headers["Cookie"]; + Assert.Equal(new[] { "name2=value2" }, cookieHeaders); + } + + private static HttpRequest CreateRequest(IHeaderDictionary headers) + { + var context = new DefaultHttpContext(); + context.Features.Get<IHttpRequestFeature>().Headers = headers; + return context.Request; + } + + private static HttpRequest GetRequestWithContentLength(string contentLength = null) + { + return GetRequestWithHeader("Content-Length", contentLength); + } + + private static HttpRequest GetRequestWithContentType(string contentType = null) + { + return GetRequestWithHeader("Content-Type", contentType); + } + + private static HttpRequest GetRequestWithAcceptHeader(string acceptHeader = null) + { + return GetRequestWithHeader("Accept", acceptHeader); + } + + private static HttpRequest GetRequestWithAcceptCharsetHeader(string acceptCharset = null) + { + return GetRequestWithHeader("Accept-Charset", acceptCharset); + } + + private static HttpRequest GetRequestWithHeader(string headerName, string headerValue) + { + var headers = new HeaderDictionary(); + if (headerValue != null) + { + headers.Add(headerName, headerValue); + } + + return CreateRequest(headers); + } + } +} diff --git a/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs b/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..4764c44a63e02fa8ddd89dcfb25387ce655f2bdf --- /dev/null +++ b/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class DefaultHttpResponseTests + { + [Theory] + [InlineData(0)] + [InlineData(9001)] + [InlineData(65535)] + public void GetContentLength_ReturnsParsedHeader(long value) + { + // Arrange + var response = GetResponseWithContentLength(value.ToString(CultureInfo.InvariantCulture)); + + // Act and Assert + Assert.Equal(value, response.ContentLength); + } + + [Fact] + public void GetContentLength_ReturnsNullIfHeaderDoesNotExist() + { + // Arrange + var response = GetResponseWithContentLength(contentLength: null); + + // Act and Assert + Assert.Null(response.ContentLength); + } + + [Theory] + [InlineData("cant-parse-this")] + [InlineData("-1000")] + [InlineData("1000.00")] + [InlineData("100/5")] + public void GetContentLength_ReturnsNullIfHeaderCannotBeParsed(string contentLength) + { + // Arrange + var response = GetResponseWithContentLength(contentLength); + + // Act and Assert + Assert.Null(response.ContentLength); + } + + [Fact] + public void GetContentType_ReturnsNullIfHeaderDoesNotExist() + { + // Arrange + var response = GetResponseWithContentType(contentType: null); + + // Act and Assert + Assert.Null(response.ContentType); + } + + private static HttpResponse CreateResponse(IHeaderDictionary headers) + { + var context = new DefaultHttpContext(); + context.Features.Get<IHttpResponseFeature>().Headers = headers; + return context.Response; + } + + private static HttpResponse GetResponseWithContentLength(string contentLength = null) + { + return GetResponseWithHeader("Content-Length", contentLength); + } + + private static HttpResponse GetResponseWithContentType(string contentType = null) + { + return GetResponseWithHeader("Content-Type", contentType); + } + + private static HttpResponse GetResponseWithHeader(string headerName, string headerValue) + { + var headers = new HeaderDictionary(); + if (headerValue != null) + { + headers.Add(headerName, headerValue); + } + + return CreateResponse(headers); + } + } +} diff --git a/src/Http/Http/test/Microsoft.AspNetCore.Http.Tests.csproj b/src/Http/Http/test/Microsoft.AspNetCore.Http.Tests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..c072fc6f67ba26c14d573fc57db869d35cf00861 --- /dev/null +++ b/src/Http/Http/test/Microsoft.AspNetCore.Http.Tests.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Http" /> + <Reference Include="Microsoft.Extensions.DependencyInjection" /> + </ItemGroup> + +</Project> diff --git a/src/Http/Http/test/RequestCookiesCollectionTests.cs b/src/Http/Http/test/RequestCookiesCollectionTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..70106df027e721919b7841db8e59f7fd5187eda8 --- /dev/null +++ b/src/Http/Http/test/RequestCookiesCollectionTests.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class RequestCookiesCollectionTests + { + public static TheoryData UnEscapesKeyValues_Data + { + get + { + // key, value, expected + return new TheoryData<string, string, string> + { + { "key=value", "key", "value" }, + { "key%2C=%21value", "key,", "!value" }, + { "ke%23y%2C=val%5Eue", "ke#y,", "val^ue" }, + { "base64=QUI%2BREU%2FRw%3D%3D", "base64", "QUI+REU/Rw==" }, + { "base64=QUI+REU/Rw==", "base64", "QUI+REU/Rw==" }, + }; + } + } + + [Theory] + [MemberData(nameof(UnEscapesKeyValues_Data))] + public void UnEscapesKeyValues( + string input, + string expectedKey, + string expectedValue) + { + var cookies = RequestCookieCollection.Parse(new StringValues(input)); + + Assert.Equal(1, cookies.Count); + Assert.Equal(expectedKey, cookies.Keys.Single()); + Assert.Equal(expectedValue, cookies[expectedKey]); + } + } +} diff --git a/src/Http/Http/test/ResponseCookiesTest.cs b/src/Http/Http/test/ResponseCookiesTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..5e5c44f89d09f6c4c2771e954140b8c72dd5a5b3 --- /dev/null +++ b/src/Http/Http/test/ResponseCookiesTest.cs @@ -0,0 +1,124 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class ResponseCookiesTest + { + [Fact] + public void DeleteCookieShouldSetDefaultPath() + { + var headers = new HeaderDictionary(); + var cookies = new ResponseCookies(headers, null); + var testcookie = "TestCookie"; + + cookies.Delete(testcookie); + + var cookieHeaderValues = headers[HeaderNames.SetCookie]; + Assert.Single(cookieHeaderValues); + Assert.StartsWith(testcookie, cookieHeaderValues[0]); + Assert.Contains("path=/", cookieHeaderValues[0]); + Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); + } + + [Fact] + public void DeleteCookieWithCookieOptionsShouldKeepPropertiesOfCookieOptions() + { + var headers = new HeaderDictionary(); + var cookies = new ResponseCookies(headers, null); + var testcookie = "TestCookie"; + var time = new DateTimeOffset(2000, 1, 1, 1, 1, 1, 1, TimeSpan.Zero); + var options = new CookieOptions + { + Secure = true, + HttpOnly = true, + Path = "/", + Expires = time, + Domain = "example.com", + SameSite = SameSiteMode.Lax + }; + + cookies.Delete(testcookie, options); + + var cookieHeaderValues = headers[HeaderNames.SetCookie]; + Assert.Single(cookieHeaderValues); + Assert.StartsWith(testcookie, cookieHeaderValues[0]); + Assert.Contains("path=/", cookieHeaderValues[0]); + Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); + Assert.Contains("secure", cookieHeaderValues[0]); + Assert.Contains("httponly", cookieHeaderValues[0]); + Assert.Contains("samesite", cookieHeaderValues[0]); + } + + [Fact] + public void NoParamsDeleteRemovesCookieCreatedByAdd() + { + var headers = new HeaderDictionary(); + var cookies = new ResponseCookies(headers, null); + var testcookie = "TestCookie"; + + cookies.Append(testcookie, testcookie); + cookies.Delete(testcookie); + + var cookieHeaderValues = headers[HeaderNames.SetCookie]; + Assert.Single(cookieHeaderValues); + Assert.StartsWith(testcookie, cookieHeaderValues[0]); + Assert.Contains("path=/", cookieHeaderValues[0]); + Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); + } + + [Fact] + public void ProvidesMaxAgeWithCookieOptionsArgumentExpectMaxAgeToBeSet() + { + var headers = new HeaderDictionary(); + var cookies = new ResponseCookies(headers, null); + var cookieOptions = new CookieOptions(); + var maxAgeTime = TimeSpan.FromHours(1); + cookieOptions.MaxAge = TimeSpan.FromHours(1); + var testcookie = "TestCookie"; + + cookies.Append(testcookie, testcookie, cookieOptions); + + var cookieHeaderValues = headers[HeaderNames.SetCookie]; + Assert.Single(cookieHeaderValues); + Assert.Contains($"max-age={maxAgeTime.TotalSeconds.ToString()}", cookieHeaderValues[0]); + } + + public static TheoryData EscapesKeyValuesBeforeSettingCookieData + { + get + { + // key, value, object pool, expected + return new TheoryData<string, string, string> + { + { "key", "value", "key=value" }, + { "key,", "!value", "key%2C=%21value" }, + { "ke#y,", "val^ue", "ke%23y%2C=val%5Eue" }, + { "base64", "QUI+REU/Rw==", "base64=QUI%2BREU%2FRw%3D%3D" }, + }; + } + } + + [Theory] + [MemberData(nameof(EscapesKeyValuesBeforeSettingCookieData))] + public void EscapesKeyValuesBeforeSettingCookie( + string key, + string value, + string expected) + { + var headers = new HeaderDictionary(); + var cookies = new ResponseCookies(headers, null); + + cookies.Append(key, value); + + var cookieHeaderValues = headers[HeaderNames.SetCookie]; + Assert.Single(cookieHeaderValues); + Assert.StartsWith(expected, cookieHeaderValues[0]); + } + } +} diff --git a/src/Http/HttpAbstractions.sln b/src/Http/HttpAbstractions.sln new file mode 100644 index 0000000000000000000000000000000000000000..7a70d0015d092a9331498813c3d50ba150d4be3c --- /dev/null +++ b/src/Http/HttpAbstractions.sln @@ -0,0 +1,312 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Authentication.Abstractions", "Authentication.Abstractions", "{587C3D55-6092-4B86-99F5-E9772C9C1ADB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Authentication.Abstractions", "Authentication.Abstractions\src\Microsoft.AspNetCore.Authentication.Abstractions.csproj", "{565B7B00-96A1-49B8-9753-9E045C6527A2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Authentication.Core", "Authentication.Core", "{B51F45A6-428F-40F4-897F-7C62C29EC39A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Authentication.Core", "Authentication.Core\src\Microsoft.AspNetCore.Authentication.Core.csproj", "{A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Authentication.Core.Test", "Authentication.Core\test\Microsoft.AspNetCore.Authentication.Core.Test.csproj", "{21071749-4361-4CD0-B5ED-541C72326800}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headers", "Headers", "{FF334B62-1AE2-477C-B91B-B28F898DFC3A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Net.Http.Headers", "Headers\src\Microsoft.Net.Http.Headers.csproj", "{D2B2E73E-A3A4-4996-906C-6647CD7D2634}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Net.Http.Headers.Tests", "Headers\test\Microsoft.Net.Http.Headers.Tests.csproj", "{9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Http", "Http", "{FB2DCA0F-EB9E-425B-ABBC-D543DBEC090F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http", "Http\src\Microsoft.AspNetCore.Http.csproj", "{E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Tests", "Http\test\Microsoft.AspNetCore.Http.Tests.csproj", "{D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Http.Abstractions", "Http.Abstractions", "{28F3D5CC-1F8E-4E15-94C8-E432DFA0A702}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Abstractions", "Http.Abstractions\src\Microsoft.AspNetCore.Http.Abstractions.csproj", "{D079CD1C-A18F-4457-91BC-432577D2FD37}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Abstractions.Tests", "Http.Abstractions\test\Microsoft.AspNetCore.Http.Abstractions.Tests.csproj", "{C28045AC-FF16-468C-A1E8-EC192DA2EF19}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Http.Extensions", "Http.Extensions", "{CCC61332-7D63-4DDB-B604-884670157624}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Extensions", "Http.Extensions\src\Microsoft.AspNetCore.Http.Extensions.csproj", "{C06F2A33-B887-46BB-8F51-2666EDBE5D38}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Extensions.Tests", "Http.Extensions\test\Microsoft.AspNetCore.Http.Extensions.Tests.csproj", "{BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Http.Features", "Http.Features", "{0B1B3E58-DA37-46D6-B791-47739EF27790}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Features", "Http.Features\src\Microsoft.AspNetCore.Http.Features.csproj", "{F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Features.Tests", "Http.Features\test\Microsoft.AspNetCore.Http.Features.Tests.csproj", "{5A64C915-7045-4100-B2CB-3A50BD854D2D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Owin", "Owin", "{4D5C4F16-5DC5-4244-A10F-08545126F61B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Owin", "Owin\src\Microsoft.AspNetCore.Owin.csproj", "{21624719-422E-4621-A17A-C6F10436F1FE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Owin.Tests", "Owin\test\Microsoft.AspNetCore.Owin.Tests.csproj", "{38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{391FBA36-BEEB-411A-A588-3F83901C0C1A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleApp", "samples\SampleApp\SampleApp.csproj", "{2378049E-ABE9-4843-AAC7-A6C9E704463D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebUtilities", "WebUtilities", "{80A090C8-ED02-4DE3-875A-30DCCDBD84BA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.WebUtilities", "WebUtilities\src\Microsoft.AspNetCore.WebUtilities.csproj", "{1A866315-5FD5-4F96-BFAC-1447E3CB4514}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.WebUtilities.Tests", "WebUtilities\test\Microsoft.AspNetCore.WebUtilities.Tests.csproj", "{068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {565B7B00-96A1-49B8-9753-9E045C6527A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {565B7B00-96A1-49B8-9753-9E045C6527A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {565B7B00-96A1-49B8-9753-9E045C6527A2}.Debug|x64.ActiveCfg = Debug|Any CPU + {565B7B00-96A1-49B8-9753-9E045C6527A2}.Debug|x64.Build.0 = Debug|Any CPU + {565B7B00-96A1-49B8-9753-9E045C6527A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {565B7B00-96A1-49B8-9753-9E045C6527A2}.Debug|x86.Build.0 = Debug|Any CPU + {565B7B00-96A1-49B8-9753-9E045C6527A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {565B7B00-96A1-49B8-9753-9E045C6527A2}.Release|Any CPU.Build.0 = Release|Any CPU + {565B7B00-96A1-49B8-9753-9E045C6527A2}.Release|x64.ActiveCfg = Release|Any CPU + {565B7B00-96A1-49B8-9753-9E045C6527A2}.Release|x64.Build.0 = Release|Any CPU + {565B7B00-96A1-49B8-9753-9E045C6527A2}.Release|x86.ActiveCfg = Release|Any CPU + {565B7B00-96A1-49B8-9753-9E045C6527A2}.Release|x86.Build.0 = Release|Any CPU + {A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}.Debug|x64.ActiveCfg = Debug|Any CPU + {A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}.Debug|x64.Build.0 = Debug|Any CPU + {A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}.Debug|x86.ActiveCfg = Debug|Any CPU + {A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}.Debug|x86.Build.0 = Debug|Any CPU + {A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}.Release|Any CPU.Build.0 = Release|Any CPU + {A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}.Release|x64.ActiveCfg = Release|Any CPU + {A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}.Release|x64.Build.0 = Release|Any CPU + {A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}.Release|x86.ActiveCfg = Release|Any CPU + {A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}.Release|x86.Build.0 = Release|Any CPU + {21071749-4361-4CD0-B5ED-541C72326800}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21071749-4361-4CD0-B5ED-541C72326800}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21071749-4361-4CD0-B5ED-541C72326800}.Debug|x64.ActiveCfg = Debug|Any CPU + {21071749-4361-4CD0-B5ED-541C72326800}.Debug|x64.Build.0 = Debug|Any CPU + {21071749-4361-4CD0-B5ED-541C72326800}.Debug|x86.ActiveCfg = Debug|Any CPU + {21071749-4361-4CD0-B5ED-541C72326800}.Debug|x86.Build.0 = Debug|Any CPU + {21071749-4361-4CD0-B5ED-541C72326800}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21071749-4361-4CD0-B5ED-541C72326800}.Release|Any CPU.Build.0 = Release|Any CPU + {21071749-4361-4CD0-B5ED-541C72326800}.Release|x64.ActiveCfg = Release|Any CPU + {21071749-4361-4CD0-B5ED-541C72326800}.Release|x64.Build.0 = Release|Any CPU + {21071749-4361-4CD0-B5ED-541C72326800}.Release|x86.ActiveCfg = Release|Any CPU + {21071749-4361-4CD0-B5ED-541C72326800}.Release|x86.Build.0 = Release|Any CPU + {D2B2E73E-A3A4-4996-906C-6647CD7D2634}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D2B2E73E-A3A4-4996-906C-6647CD7D2634}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2B2E73E-A3A4-4996-906C-6647CD7D2634}.Debug|x64.ActiveCfg = Debug|Any CPU + {D2B2E73E-A3A4-4996-906C-6647CD7D2634}.Debug|x64.Build.0 = Debug|Any CPU + {D2B2E73E-A3A4-4996-906C-6647CD7D2634}.Debug|x86.ActiveCfg = Debug|Any CPU + {D2B2E73E-A3A4-4996-906C-6647CD7D2634}.Debug|x86.Build.0 = Debug|Any CPU + {D2B2E73E-A3A4-4996-906C-6647CD7D2634}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D2B2E73E-A3A4-4996-906C-6647CD7D2634}.Release|Any CPU.Build.0 = Release|Any CPU + {D2B2E73E-A3A4-4996-906C-6647CD7D2634}.Release|x64.ActiveCfg = Release|Any CPU + {D2B2E73E-A3A4-4996-906C-6647CD7D2634}.Release|x64.Build.0 = Release|Any CPU + {D2B2E73E-A3A4-4996-906C-6647CD7D2634}.Release|x86.ActiveCfg = Release|Any CPU + {D2B2E73E-A3A4-4996-906C-6647CD7D2634}.Release|x86.Build.0 = Release|Any CPU + {9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}.Debug|x64.ActiveCfg = Debug|Any CPU + {9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}.Debug|x64.Build.0 = Debug|Any CPU + {9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}.Debug|x86.ActiveCfg = Debug|Any CPU + {9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}.Debug|x86.Build.0 = Debug|Any CPU + {9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}.Release|Any CPU.Build.0 = Release|Any CPU + {9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}.Release|x64.ActiveCfg = Release|Any CPU + {9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}.Release|x64.Build.0 = Release|Any CPU + {9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}.Release|x86.ActiveCfg = Release|Any CPU + {9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}.Release|x86.Build.0 = Release|Any CPU + {E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}.Debug|x64.ActiveCfg = Debug|Any CPU + {E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}.Debug|x64.Build.0 = Debug|Any CPU + {E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}.Debug|x86.ActiveCfg = Debug|Any CPU + {E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}.Debug|x86.Build.0 = Debug|Any CPU + {E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}.Release|Any CPU.Build.0 = Release|Any CPU + {E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}.Release|x64.ActiveCfg = Release|Any CPU + {E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}.Release|x64.Build.0 = Release|Any CPU + {E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}.Release|x86.ActiveCfg = Release|Any CPU + {E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}.Release|x86.Build.0 = Release|Any CPU + {D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}.Debug|x64.ActiveCfg = Debug|Any CPU + {D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}.Debug|x64.Build.0 = Debug|Any CPU + {D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}.Debug|x86.ActiveCfg = Debug|Any CPU + {D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}.Debug|x86.Build.0 = Debug|Any CPU + {D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}.Release|Any CPU.Build.0 = Release|Any CPU + {D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}.Release|x64.ActiveCfg = Release|Any CPU + {D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}.Release|x64.Build.0 = Release|Any CPU + {D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}.Release|x86.ActiveCfg = Release|Any CPU + {D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}.Release|x86.Build.0 = Release|Any CPU + {D079CD1C-A18F-4457-91BC-432577D2FD37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D079CD1C-A18F-4457-91BC-432577D2FD37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D079CD1C-A18F-4457-91BC-432577D2FD37}.Debug|x64.ActiveCfg = Debug|Any CPU + {D079CD1C-A18F-4457-91BC-432577D2FD37}.Debug|x64.Build.0 = Debug|Any CPU + {D079CD1C-A18F-4457-91BC-432577D2FD37}.Debug|x86.ActiveCfg = Debug|Any CPU + {D079CD1C-A18F-4457-91BC-432577D2FD37}.Debug|x86.Build.0 = Debug|Any CPU + {D079CD1C-A18F-4457-91BC-432577D2FD37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D079CD1C-A18F-4457-91BC-432577D2FD37}.Release|Any CPU.Build.0 = Release|Any CPU + {D079CD1C-A18F-4457-91BC-432577D2FD37}.Release|x64.ActiveCfg = Release|Any CPU + {D079CD1C-A18F-4457-91BC-432577D2FD37}.Release|x64.Build.0 = Release|Any CPU + {D079CD1C-A18F-4457-91BC-432577D2FD37}.Release|x86.ActiveCfg = Release|Any CPU + {D079CD1C-A18F-4457-91BC-432577D2FD37}.Release|x86.Build.0 = Release|Any CPU + {C28045AC-FF16-468C-A1E8-EC192DA2EF19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C28045AC-FF16-468C-A1E8-EC192DA2EF19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C28045AC-FF16-468C-A1E8-EC192DA2EF19}.Debug|x64.ActiveCfg = Debug|Any CPU + {C28045AC-FF16-468C-A1E8-EC192DA2EF19}.Debug|x64.Build.0 = Debug|Any CPU + {C28045AC-FF16-468C-A1E8-EC192DA2EF19}.Debug|x86.ActiveCfg = Debug|Any CPU + {C28045AC-FF16-468C-A1E8-EC192DA2EF19}.Debug|x86.Build.0 = Debug|Any CPU + {C28045AC-FF16-468C-A1E8-EC192DA2EF19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C28045AC-FF16-468C-A1E8-EC192DA2EF19}.Release|Any CPU.Build.0 = Release|Any CPU + {C28045AC-FF16-468C-A1E8-EC192DA2EF19}.Release|x64.ActiveCfg = Release|Any CPU + {C28045AC-FF16-468C-A1E8-EC192DA2EF19}.Release|x64.Build.0 = Release|Any CPU + {C28045AC-FF16-468C-A1E8-EC192DA2EF19}.Release|x86.ActiveCfg = Release|Any CPU + {C28045AC-FF16-468C-A1E8-EC192DA2EF19}.Release|x86.Build.0 = Release|Any CPU + {C06F2A33-B887-46BB-8F51-2666EDBE5D38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C06F2A33-B887-46BB-8F51-2666EDBE5D38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C06F2A33-B887-46BB-8F51-2666EDBE5D38}.Debug|x64.ActiveCfg = Debug|Any CPU + {C06F2A33-B887-46BB-8F51-2666EDBE5D38}.Debug|x64.Build.0 = Debug|Any CPU + {C06F2A33-B887-46BB-8F51-2666EDBE5D38}.Debug|x86.ActiveCfg = Debug|Any CPU + {C06F2A33-B887-46BB-8F51-2666EDBE5D38}.Debug|x86.Build.0 = Debug|Any CPU + {C06F2A33-B887-46BB-8F51-2666EDBE5D38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C06F2A33-B887-46BB-8F51-2666EDBE5D38}.Release|Any CPU.Build.0 = Release|Any CPU + {C06F2A33-B887-46BB-8F51-2666EDBE5D38}.Release|x64.ActiveCfg = Release|Any CPU + {C06F2A33-B887-46BB-8F51-2666EDBE5D38}.Release|x64.Build.0 = Release|Any CPU + {C06F2A33-B887-46BB-8F51-2666EDBE5D38}.Release|x86.ActiveCfg = Release|Any CPU + {C06F2A33-B887-46BB-8F51-2666EDBE5D38}.Release|x86.Build.0 = Release|Any CPU + {BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}.Debug|x64.ActiveCfg = Debug|Any CPU + {BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}.Debug|x64.Build.0 = Debug|Any CPU + {BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}.Debug|x86.ActiveCfg = Debug|Any CPU + {BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}.Debug|x86.Build.0 = Debug|Any CPU + {BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}.Release|Any CPU.Build.0 = Release|Any CPU + {BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}.Release|x64.ActiveCfg = Release|Any CPU + {BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}.Release|x64.Build.0 = Release|Any CPU + {BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}.Release|x86.ActiveCfg = Release|Any CPU + {BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}.Release|x86.Build.0 = Release|Any CPU + {F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}.Debug|x64.ActiveCfg = Debug|Any CPU + {F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}.Debug|x64.Build.0 = Debug|Any CPU + {F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}.Debug|x86.ActiveCfg = Debug|Any CPU + {F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}.Debug|x86.Build.0 = Debug|Any CPU + {F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}.Release|Any CPU.Build.0 = Release|Any CPU + {F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}.Release|x64.ActiveCfg = Release|Any CPU + {F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}.Release|x64.Build.0 = Release|Any CPU + {F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}.Release|x86.ActiveCfg = Release|Any CPU + {F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}.Release|x86.Build.0 = Release|Any CPU + {5A64C915-7045-4100-B2CB-3A50BD854D2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A64C915-7045-4100-B2CB-3A50BD854D2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A64C915-7045-4100-B2CB-3A50BD854D2D}.Debug|x64.ActiveCfg = Debug|Any CPU + {5A64C915-7045-4100-B2CB-3A50BD854D2D}.Debug|x64.Build.0 = Debug|Any CPU + {5A64C915-7045-4100-B2CB-3A50BD854D2D}.Debug|x86.ActiveCfg = Debug|Any CPU + {5A64C915-7045-4100-B2CB-3A50BD854D2D}.Debug|x86.Build.0 = Debug|Any CPU + {5A64C915-7045-4100-B2CB-3A50BD854D2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A64C915-7045-4100-B2CB-3A50BD854D2D}.Release|Any CPU.Build.0 = Release|Any CPU + {5A64C915-7045-4100-B2CB-3A50BD854D2D}.Release|x64.ActiveCfg = Release|Any CPU + {5A64C915-7045-4100-B2CB-3A50BD854D2D}.Release|x64.Build.0 = Release|Any CPU + {5A64C915-7045-4100-B2CB-3A50BD854D2D}.Release|x86.ActiveCfg = Release|Any CPU + {5A64C915-7045-4100-B2CB-3A50BD854D2D}.Release|x86.Build.0 = Release|Any CPU + {21624719-422E-4621-A17A-C6F10436F1FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21624719-422E-4621-A17A-C6F10436F1FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21624719-422E-4621-A17A-C6F10436F1FE}.Debug|x64.ActiveCfg = Debug|Any CPU + {21624719-422E-4621-A17A-C6F10436F1FE}.Debug|x64.Build.0 = Debug|Any CPU + {21624719-422E-4621-A17A-C6F10436F1FE}.Debug|x86.ActiveCfg = Debug|Any CPU + {21624719-422E-4621-A17A-C6F10436F1FE}.Debug|x86.Build.0 = Debug|Any CPU + {21624719-422E-4621-A17A-C6F10436F1FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21624719-422E-4621-A17A-C6F10436F1FE}.Release|Any CPU.Build.0 = Release|Any CPU + {21624719-422E-4621-A17A-C6F10436F1FE}.Release|x64.ActiveCfg = Release|Any CPU + {21624719-422E-4621-A17A-C6F10436F1FE}.Release|x64.Build.0 = Release|Any CPU + {21624719-422E-4621-A17A-C6F10436F1FE}.Release|x86.ActiveCfg = Release|Any CPU + {21624719-422E-4621-A17A-C6F10436F1FE}.Release|x86.Build.0 = Release|Any CPU + {38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}.Debug|x64.ActiveCfg = Debug|Any CPU + {38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}.Debug|x64.Build.0 = Debug|Any CPU + {38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}.Debug|x86.ActiveCfg = Debug|Any CPU + {38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}.Debug|x86.Build.0 = Debug|Any CPU + {38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}.Release|Any CPU.Build.0 = Release|Any CPU + {38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}.Release|x64.ActiveCfg = Release|Any CPU + {38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}.Release|x64.Build.0 = Release|Any CPU + {38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}.Release|x86.ActiveCfg = Release|Any CPU + {38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}.Release|x86.Build.0 = Release|Any CPU + {2378049E-ABE9-4843-AAC7-A6C9E704463D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2378049E-ABE9-4843-AAC7-A6C9E704463D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2378049E-ABE9-4843-AAC7-A6C9E704463D}.Debug|x64.ActiveCfg = Debug|Any CPU + {2378049E-ABE9-4843-AAC7-A6C9E704463D}.Debug|x64.Build.0 = Debug|Any CPU + {2378049E-ABE9-4843-AAC7-A6C9E704463D}.Debug|x86.ActiveCfg = Debug|Any CPU + {2378049E-ABE9-4843-AAC7-A6C9E704463D}.Debug|x86.Build.0 = Debug|Any CPU + {2378049E-ABE9-4843-AAC7-A6C9E704463D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2378049E-ABE9-4843-AAC7-A6C9E704463D}.Release|Any CPU.Build.0 = Release|Any CPU + {2378049E-ABE9-4843-AAC7-A6C9E704463D}.Release|x64.ActiveCfg = Release|Any CPU + {2378049E-ABE9-4843-AAC7-A6C9E704463D}.Release|x64.Build.0 = Release|Any CPU + {2378049E-ABE9-4843-AAC7-A6C9E704463D}.Release|x86.ActiveCfg = Release|Any CPU + {2378049E-ABE9-4843-AAC7-A6C9E704463D}.Release|x86.Build.0 = Release|Any CPU + {1A866315-5FD5-4F96-BFAC-1447E3CB4514}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A866315-5FD5-4F96-BFAC-1447E3CB4514}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A866315-5FD5-4F96-BFAC-1447E3CB4514}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A866315-5FD5-4F96-BFAC-1447E3CB4514}.Debug|x64.Build.0 = Debug|Any CPU + {1A866315-5FD5-4F96-BFAC-1447E3CB4514}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A866315-5FD5-4F96-BFAC-1447E3CB4514}.Debug|x86.Build.0 = Debug|Any CPU + {1A866315-5FD5-4F96-BFAC-1447E3CB4514}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A866315-5FD5-4F96-BFAC-1447E3CB4514}.Release|Any CPU.Build.0 = Release|Any CPU + {1A866315-5FD5-4F96-BFAC-1447E3CB4514}.Release|x64.ActiveCfg = Release|Any CPU + {1A866315-5FD5-4F96-BFAC-1447E3CB4514}.Release|x64.Build.0 = Release|Any CPU + {1A866315-5FD5-4F96-BFAC-1447E3CB4514}.Release|x86.ActiveCfg = Release|Any CPU + {1A866315-5FD5-4F96-BFAC-1447E3CB4514}.Release|x86.Build.0 = Release|Any CPU + {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Debug|x64.ActiveCfg = Debug|Any CPU + {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Debug|x64.Build.0 = Debug|Any CPU + {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Debug|x86.ActiveCfg = Debug|Any CPU + {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Debug|x86.Build.0 = Debug|Any CPU + {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Release|Any CPU.Build.0 = Release|Any CPU + {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Release|x64.ActiveCfg = Release|Any CPU + {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Release|x64.Build.0 = Release|Any CPU + {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Release|x86.ActiveCfg = Release|Any CPU + {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {565B7B00-96A1-49B8-9753-9E045C6527A2} = {587C3D55-6092-4B86-99F5-E9772C9C1ADB} + {A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96} = {B51F45A6-428F-40F4-897F-7C62C29EC39A} + {21071749-4361-4CD0-B5ED-541C72326800} = {B51F45A6-428F-40F4-897F-7C62C29EC39A} + {D2B2E73E-A3A4-4996-906C-6647CD7D2634} = {FF334B62-1AE2-477C-B91B-B28F898DFC3A} + {9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF} = {FF334B62-1AE2-477C-B91B-B28F898DFC3A} + {E35F0A95-0016-4B4D-BB85-ADB4CFAD857F} = {FB2DCA0F-EB9E-425B-ABBC-D543DBEC090F} + {D9155D31-0844-4ED6-AC7B-6C4C9DA6E891} = {FB2DCA0F-EB9E-425B-ABBC-D543DBEC090F} + {D079CD1C-A18F-4457-91BC-432577D2FD37} = {28F3D5CC-1F8E-4E15-94C8-E432DFA0A702} + {C28045AC-FF16-468C-A1E8-EC192DA2EF19} = {28F3D5CC-1F8E-4E15-94C8-E432DFA0A702} + {C06F2A33-B887-46BB-8F51-2666EDBE5D38} = {CCC61332-7D63-4DDB-B604-884670157624} + {BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07} = {CCC61332-7D63-4DDB-B604-884670157624} + {F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6} = {0B1B3E58-DA37-46D6-B791-47739EF27790} + {5A64C915-7045-4100-B2CB-3A50BD854D2D} = {0B1B3E58-DA37-46D6-B791-47739EF27790} + {21624719-422E-4621-A17A-C6F10436F1FE} = {4D5C4F16-5DC5-4244-A10F-08545126F61B} + {38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4} = {4D5C4F16-5DC5-4244-A10F-08545126F61B} + {2378049E-ABE9-4843-AAC7-A6C9E704463D} = {391FBA36-BEEB-411A-A588-3F83901C0C1A} + {1A866315-5FD5-4F96-BFAC-1447E3CB4514} = {80A090C8-ED02-4DE3-875A-30DCCDBD84BA} + {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8} = {80A090C8-ED02-4DE3-875A-30DCCDBD84BA} + EndGlobalSection +EndGlobal diff --git a/src/Http/Owin/src/DictionaryStringArrayWrapper.cs b/src/Http/Owin/src/DictionaryStringArrayWrapper.cs new file mode 100644 index 0000000000000000000000000000000000000000..c4bb38f386363cd264dd9faad80259bb274be02d --- /dev/null +++ b/src/Http/Owin/src/DictionaryStringArrayWrapper.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Owin +{ + internal class DictionaryStringArrayWrapper : IDictionary<string, string[]> + { + public DictionaryStringArrayWrapper(IHeaderDictionary inner) + { + Inner = inner; + } + + public readonly IHeaderDictionary Inner; + + private KeyValuePair<string, StringValues> Convert(KeyValuePair<string, string[]> item) => new KeyValuePair<string, StringValues>(item.Key, item.Value); + + private KeyValuePair<string, string[]> Convert(KeyValuePair<string, StringValues> item) => new KeyValuePair<string, string[]>(item.Key, item.Value); + + private StringValues Convert(string[] item) => item; + + private string[] Convert(StringValues item) => item; + + string[] IDictionary<string, string[]>.this[string key] + { + get { return ((IDictionary<string, StringValues>)Inner)[key]; } + set { Inner[key] = value; } + } + + int ICollection<KeyValuePair<string, string[]>>.Count => Inner.Count; + + bool ICollection<KeyValuePair<string, string[]>>.IsReadOnly => Inner.IsReadOnly; + + ICollection<string> IDictionary<string, string[]>.Keys => Inner.Keys; + + ICollection<string[]> IDictionary<string, string[]>.Values => Inner.Values.Select(Convert).ToList(); + + void ICollection<KeyValuePair<string, string[]>>.Add(KeyValuePair<string, string[]> item) => Inner.Add(Convert(item)); + + void IDictionary<string, string[]>.Add(string key, string[] value) => Inner.Add(key, value); + + void ICollection<KeyValuePair<string, string[]>>.Clear() => Inner.Clear(); + + bool ICollection<KeyValuePair<string, string[]>>.Contains(KeyValuePair<string, string[]> item) => Inner.Contains(Convert(item)); + + bool IDictionary<string, string[]>.ContainsKey(string key) => Inner.ContainsKey(key); + + void ICollection<KeyValuePair<string, string[]>>.CopyTo(KeyValuePair<string, string[]>[] array, int arrayIndex) + { + foreach(var kv in Inner) + { + array[arrayIndex++] = Convert(kv); + } + } + + IEnumerator IEnumerable.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + + IEnumerator<KeyValuePair<string, string[]>> IEnumerable<KeyValuePair<string, string[]>>.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + + bool ICollection<KeyValuePair<string, string[]>>.Remove(KeyValuePair<string, string[]> item) => Inner.Remove(Convert(item)); + + bool IDictionary<string, string[]>.Remove(string key) => Inner.Remove(key); + + bool IDictionary<string, string[]>.TryGetValue(string key, out string[] value) + { + StringValues temp; + if (Inner.TryGetValue(key, out temp)) + { + value = temp; + return true; + } + value = default(StringValues); + return false; + } + } +} diff --git a/src/Http/Owin/src/DictionaryStringValuesWrapper.cs b/src/Http/Owin/src/DictionaryStringValuesWrapper.cs new file mode 100644 index 0000000000000000000000000000000000000000..b31c7e9790c1e79d567bbe2c8cb8d0909771240a --- /dev/null +++ b/src/Http/Owin/src/DictionaryStringValuesWrapper.cs @@ -0,0 +1,126 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Owin +{ + internal class DictionaryStringValuesWrapper : IHeaderDictionary + { + public DictionaryStringValuesWrapper(IDictionary<string, string[]> inner) + { + Inner = inner; + } + + public readonly IDictionary<string, string[]> Inner; + + private KeyValuePair<string, StringValues> Convert(KeyValuePair<string, string[]> item) => new KeyValuePair<string, StringValues>(item.Key, item.Value); + + private KeyValuePair<string, string[]> Convert(KeyValuePair<string, StringValues> item) => new KeyValuePair<string, string[]>(item.Key, item.Value); + + private StringValues Convert(string[] item) => item; + + private string[] Convert(StringValues item) => item; + + StringValues IHeaderDictionary.this[string key] + { + get + { + string[] values; + return Inner.TryGetValue(key, out values) ? values : null; + } + set { Inner[key] = value; } + } + + StringValues IDictionary<string, StringValues>.this[string key] + { + get { return Inner[key]; } + set { Inner[key] = value; } + } + + public long? ContentLength + { + get + { + long value; + + string[] rawValue; + if (!Inner.TryGetValue(HeaderNames.ContentLength, out rawValue)) + { + return null; + } + + if (rawValue.Length == 1 && + !string.IsNullOrEmpty(rawValue[0]) && + HeaderUtilities.TryParseNonNegativeInt64(new StringSegment(rawValue[0]).Trim(), out value)) + { + return value; + } + + return null; + } + set + { + if (value.HasValue) + { + Inner[HeaderNames.ContentLength] = (StringValues)HeaderUtilities.FormatNonNegativeInt64(value.Value); + } + else + { + Inner.Remove(HeaderNames.ContentLength); + } + } + } + + int ICollection<KeyValuePair<string, StringValues>>.Count => Inner.Count; + + bool ICollection<KeyValuePair<string, StringValues>>.IsReadOnly => Inner.IsReadOnly; + + ICollection<string> IDictionary<string, StringValues>.Keys => Inner.Keys; + + ICollection<StringValues> IDictionary<string, StringValues>.Values => Inner.Values.Select(Convert).ToList(); + + void ICollection<KeyValuePair<string, StringValues>>.Add(KeyValuePair<string, StringValues> item) => Inner.Add(Convert(item)); + + void IDictionary<string, StringValues>.Add(string key, StringValues value) => Inner.Add(key, value); + + void ICollection<KeyValuePair<string, StringValues>>.Clear() => Inner.Clear(); + + bool ICollection<KeyValuePair<string, StringValues>>.Contains(KeyValuePair<string, StringValues> item) => Inner.Contains(Convert(item)); + + bool IDictionary<string, StringValues>.ContainsKey(string key) => Inner.ContainsKey(key); + + void ICollection<KeyValuePair<string, StringValues>>.CopyTo(KeyValuePair<string, StringValues>[] array, int arrayIndex) + { + foreach (var kv in Inner) + { + array[arrayIndex++] = Convert(kv); + } + } + + IEnumerator IEnumerable.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + + IEnumerator<KeyValuePair<string, StringValues>> IEnumerable<KeyValuePair<string, StringValues>>.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + + bool ICollection<KeyValuePair<string, StringValues>>.Remove(KeyValuePair<string, StringValues> item) => Inner.Remove(Convert(item)); + + bool IDictionary<string, StringValues>.Remove(string key) => Inner.Remove(key); + + bool IDictionary<string, StringValues>.TryGetValue(string key, out StringValues value) + { + string[] temp; + if (Inner.TryGetValue(key, out temp)) + { + value = temp; + return true; + } + value = default(StringValues); + return false; + } + } +} diff --git a/src/Http/Owin/src/IOwinEnvironmentFeature.cs b/src/Http/Owin/src/IOwinEnvironmentFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..8a476b9f38f2b49d903ac8aedae5f2382355861c --- /dev/null +++ b/src/Http/Owin/src/IOwinEnvironmentFeature.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Owin +{ + public interface IOwinEnvironmentFeature + { + IDictionary<string, object> Environment { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Owin/src/Microsoft.AspNetCore.Owin.csproj b/src/Http/Owin/src/Microsoft.AspNetCore.Owin.csproj new file mode 100644 index 0000000000000000000000000000000000000000..cf9574d7f82a85eac8104257543c203aa69ac04b --- /dev/null +++ b/src/Http/Owin/src/Microsoft.AspNetCore.Owin.csproj @@ -0,0 +1,15 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>ASP.NET Core component for running OWIN middleware in an ASP.NET Core application, and to run ASP.NET Core middleware in an OWIN application.</Description> + <TargetFramework>netstandard2.0</TargetFramework> + <NoWarn>$(NoWarn);CS1591</NoWarn> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore;owin</PackageTags> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Http" /> + </ItemGroup> + +</Project> diff --git a/src/Http/Owin/src/OwinConstants.cs b/src/Http/Owin/src/OwinConstants.cs new file mode 100644 index 0000000000000000000000000000000000000000..4234b65aa68516cfd49bd6172c14902e7f5ce835 --- /dev/null +++ b/src/Http/Owin/src/OwinConstants.cs @@ -0,0 +1,177 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Owin +{ + internal static class OwinConstants + { + #region OWIN v1.0.0 - 3.2.1. Request Data + + // http://owin.org/spec/spec/owin-1.0.0.html + + public const string RequestScheme = "owin.RequestScheme"; + public const string RequestMethod = "owin.RequestMethod"; + public const string RequestPathBase = "owin.RequestPathBase"; + public const string RequestPath = "owin.RequestPath"; + public const string RequestQueryString = "owin.RequestQueryString"; + public const string RequestProtocol = "owin.RequestProtocol"; + public const string RequestHeaders = "owin.RequestHeaders"; + public const string RequestBody = "owin.RequestBody"; + + #endregion + + #region OWIN v1.0.1 - 3.2.1 Request Data + + // OWIN 1.0.1 http://owin.org/html/owin.html + + public const string RequestId = "owin.RequestId"; + public const string RequestUser = "owin.RequestUser"; + + #endregion + + #region OWIN v1.0.0 - 3.2.2. Response Data + + // http://owin.org/spec/spec/owin-1.0.0.html + + public const string ResponseStatusCode = "owin.ResponseStatusCode"; + public const string ResponseReasonPhrase = "owin.ResponseReasonPhrase"; + public const string ResponseProtocol = "owin.ResponseProtocol"; + public const string ResponseHeaders = "owin.ResponseHeaders"; + public const string ResponseBody = "owin.ResponseBody"; + + #endregion + + #region OWIN v1.0.0 - 3.2.3. Other Data + + // http://owin.org/spec/spec/owin-1.0.0.html + + public const string CallCancelled = "owin.CallCancelled"; + + public const string OwinVersion = "owin.Version"; + + #endregion + + #region OWIN Keys for IAppBuilder.Properties + + internal static class Builder + { + public const string AddSignatureConversion = "builder.AddSignatureConversion"; + public const string DefaultApp = "builder.DefaultApp"; + } + + #endregion + + #region OWIN Key Guidelines and Common Keys - 6. Common keys + + // http://owin.org/spec/spec/CommonKeys.html + + internal static class CommonKeys + { + public const string ClientCertificate = "ssl.ClientCertificate"; + public const string LoadClientCertAsync = "ssl.LoadClientCertAsync"; + public const string RemoteIpAddress = "server.RemoteIpAddress"; + public const string RemotePort = "server.RemotePort"; + public const string LocalIpAddress = "server.LocalIpAddress"; + public const string LocalPort = "server.LocalPort"; + public const string ConnectionId = "server.ConnectionId"; + public const string TraceOutput = "host.TraceOutput"; + public const string Addresses = "host.Addresses"; + public const string AppName = "host.AppName"; + public const string Capabilities = "server.Capabilities"; + public const string OnSendingHeaders = "server.OnSendingHeaders"; + public const string OnAppDisposing = "host.OnAppDisposing"; + public const string Scheme = "scheme"; + public const string Host = "host"; + public const string Port = "port"; + public const string Path = "path"; + } + + #endregion + + #region SendFiles v0.3.0 + + // http://owin.org/spec/extensions/owin-SendFile-Extension-v0.3.0.htm + + internal static class SendFiles + { + // 3.1. Startup + + public const string Version = "sendfile.Version"; + public const string Support = "sendfile.Support"; + public const string Concurrency = "sendfile.Concurrency"; + + // 3.2. Per Request + + public const string SendAsync = "sendfile.SendAsync"; + } + + #endregion + + #region Opaque v0.3.0 + + // http://owin.org/spec/extensions/owin-OpaqueStream-Extension-v0.3.0.htm + + internal static class OpaqueConstants + { + // 3.1. Startup + + public const string Version = "opaque.Version"; + + // 3.2. Per Request + + public const string Upgrade = "opaque.Upgrade"; + + // 5. Consumption + + public const string Stream = "opaque.Stream"; + // public const string Version = "opaque.Version"; // redundant, declared above + public const string CallCancelled = "opaque.CallCancelled"; + } + + #endregion + + #region WebSocket v0.4.0 + + // http://owin.org/spec/extensions/owin-OpaqueStream-Extension-v0.3.0.htm + + internal static class WebSocket + { + // 3.1. Startup + + public const string Version = "websocket.Version"; + public const string VersionValue = "1.0"; + + // 3.2. Per Request + + public const string Accept = "websocket.Accept"; + public const string AcceptAlt = "websocket.AcceptAlt"; // Non-spec + + // 4. Accept + + public const string SubProtocol = "websocket.SubProtocol"; + + // 5. Consumption + + public const string SendAsync = "websocket.SendAsync"; + public const string ReceiveAsync = "websocket.ReceiveAsync"; + public const string CloseAsync = "websocket.CloseAsync"; + // public const string Version = "websocket.Version"; // redundant, declared above + public const string CallCancelled = "websocket.CallCancelled"; + public const string ClientCloseStatus = "websocket.ClientCloseStatus"; + public const string ClientCloseDescription = "websocket.ClientCloseDescription"; + } + + #endregion + + #region Security v0.1.0 + + internal static class Security + { + // 3.2. Per Request + + public const string User = "server.User"; + } + + #endregion + } +} diff --git a/src/Http/Owin/src/OwinEnvironment.cs b/src/Http/Owin/src/OwinEnvironment.cs new file mode 100644 index 0000000000000000000000000000000000000000..6c7f3ad66f0cad135f33fc7a5af3eb8fbcdb4bba --- /dev/null +++ b/src/Http/Owin/src/OwinEnvironment.cs @@ -0,0 +1,397 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; + +namespace Microsoft.AspNetCore.Owin +{ + using SendFileFunc = Func<string, long, long?, CancellationToken, Task>; + using WebSocketAcceptAlt = + Func + < + WebSocketAcceptContext, // WebSocket Accept parameters + Task<WebSocket> + >; + + public class OwinEnvironment : IDictionary<string, object> + { + private HttpContext _context; + private IDictionary<string, FeatureMap> _entries; + + public OwinEnvironment(HttpContext context) + { + if (context.Features.Get<IHttpRequestFeature>() == null) + { + throw new ArgumentException("Missing required feature: " + nameof(IHttpRequestFeature) + ".", nameof(context)); + } + if (context.Features.Get<IHttpResponseFeature>() == null) + { + throw new ArgumentException("Missing required feature: " + nameof(IHttpResponseFeature) + ".", nameof(context)); + } + + _context = context; + _entries = new Dictionary<string, FeatureMap>() + { + { OwinConstants.RequestProtocol, new FeatureMap<IHttpRequestFeature>(feature => feature.Protocol, () => string.Empty, (feature, value) => feature.Protocol = Convert.ToString(value)) }, + { OwinConstants.RequestScheme, new FeatureMap<IHttpRequestFeature>(feature => feature.Scheme, () => string.Empty, (feature, value) => feature.Scheme = Convert.ToString(value)) }, + { OwinConstants.RequestMethod, new FeatureMap<IHttpRequestFeature>(feature => feature.Method, () => string.Empty, (feature, value) => feature.Method = Convert.ToString(value)) }, + { OwinConstants.RequestPathBase, new FeatureMap<IHttpRequestFeature>(feature => feature.PathBase, () => string.Empty, (feature, value) => feature.PathBase = Convert.ToString(value)) }, + { OwinConstants.RequestPath, new FeatureMap<IHttpRequestFeature>(feature => feature.Path, () => string.Empty, (feature, value) => feature.Path = Convert.ToString(value)) }, + { OwinConstants.RequestQueryString, new FeatureMap<IHttpRequestFeature>(feature => Utilities.RemoveQuestionMark(feature.QueryString), () => string.Empty, + (feature, value) => feature.QueryString = Utilities.AddQuestionMark(Convert.ToString(value))) }, + { OwinConstants.RequestHeaders, new FeatureMap<IHttpRequestFeature>(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary<string, string[]>)value)) }, + { OwinConstants.RequestBody, new FeatureMap<IHttpRequestFeature>(feature => feature.Body, () => Stream.Null, (feature, value) => feature.Body = (Stream)value) }, + { OwinConstants.RequestUser, new FeatureMap<IHttpAuthenticationFeature>(feature => feature.User, () => null, (feature, value) => feature.User = (ClaimsPrincipal)value) }, + + { OwinConstants.ResponseStatusCode, new FeatureMap<IHttpResponseFeature>(feature => feature.StatusCode, () => 200, (feature, value) => feature.StatusCode = Convert.ToInt32(value)) }, + { OwinConstants.ResponseReasonPhrase, new FeatureMap<IHttpResponseFeature>(feature => feature.ReasonPhrase, (feature, value) => feature.ReasonPhrase = Convert.ToString(value)) }, + { OwinConstants.ResponseHeaders, new FeatureMap<IHttpResponseFeature>(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary<string, string[]>)value)) }, + { OwinConstants.ResponseBody, new FeatureMap<IHttpResponseFeature>(feature => feature.Body, () => Stream.Null, (feature, value) => feature.Body = (Stream)value) }, + { OwinConstants.CommonKeys.OnSendingHeaders, new FeatureMap<IHttpResponseFeature>( + feature => new Action<Action<object>, object>((cb, state) => { + feature.OnStarting(s => + { + cb(s); + return Task.CompletedTask; + }, state); + })) + }, + + { OwinConstants.CommonKeys.ConnectionId, new FeatureMap<IHttpConnectionFeature>(feature => feature.ConnectionId, + (feature, value) => feature.ConnectionId = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + + { OwinConstants.CommonKeys.LocalPort, new FeatureMap<IHttpConnectionFeature>(feature => feature.LocalPort.ToString(CultureInfo.InvariantCulture), + (feature, value) => feature.LocalPort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.CommonKeys.RemotePort, new FeatureMap<IHttpConnectionFeature>(feature => feature.RemotePort.ToString(CultureInfo.InvariantCulture), + (feature, value) => feature.RemotePort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, + + { OwinConstants.CommonKeys.LocalIpAddress, new FeatureMap<IHttpConnectionFeature>(feature => feature.LocalIpAddress.ToString(), + (feature, value) => feature.LocalIpAddress = IPAddress.Parse(Convert.ToString(value))) }, + { OwinConstants.CommonKeys.RemoteIpAddress, new FeatureMap<IHttpConnectionFeature>(feature => feature.RemoteIpAddress.ToString(), + (feature, value) => feature.RemoteIpAddress = IPAddress.Parse(Convert.ToString(value))) }, + + { OwinConstants.SendFiles.SendAsync, new FeatureMap<IHttpSendFileFeature>(feature => new SendFileFunc(feature.SendFileAsync)) }, + + { OwinConstants.Security.User, new FeatureMap<IHttpAuthenticationFeature>(feature => feature.User, + ()=> null, (feature, value) => feature.User = Utilities.MakeClaimsPrincipal((IPrincipal)value), + () => new HttpAuthenticationFeature()) + }, + + { OwinConstants.RequestId, new FeatureMap<IHttpRequestIdentifierFeature>(feature => feature.TraceIdentifier, + ()=> null, (feature, value) => feature.TraceIdentifier = (string)value, + () => new HttpRequestIdentifierFeature()) + } + }; + + // owin.CallCancelled is required but the feature may not be present. + if (context.Features.Get<IHttpRequestLifetimeFeature>() != null) + { + _entries[OwinConstants.CallCancelled] = new FeatureMap<IHttpRequestLifetimeFeature>(feature => feature.RequestAborted); + } + else if (!_context.Items.ContainsKey(OwinConstants.CallCancelled)) + { + _context.Items[OwinConstants.CallCancelled] = CancellationToken.None; + } + + // owin.Version is required. + if (!context.Items.ContainsKey(OwinConstants.OwinVersion)) + { + _context.Items[OwinConstants.OwinVersion] = "1.0"; + } + + if (context.Request.IsHttps) + { + _entries.Add(OwinConstants.CommonKeys.ClientCertificate, new FeatureMap<ITlsConnectionFeature>(feature => feature.ClientCertificate, + (feature, value) => feature.ClientCertificate = (X509Certificate2)value)); + _entries.Add(OwinConstants.CommonKeys.LoadClientCertAsync, new FeatureMap<ITlsConnectionFeature>( + feature => new Func<Task>(() => feature.GetClientCertificateAsync(CancellationToken.None)))); + } + + if (context.WebSockets.IsWebSocketRequest) + { + _entries.Add(OwinConstants.WebSocket.AcceptAlt, new FeatureMap<IHttpWebSocketFeature>(feature => new WebSocketAcceptAlt(feature.AcceptAsync))); + } + + _context.Items[typeof(HttpContext).FullName] = _context; // Store for lookup when we transition back out of OWIN + } + + // Public in case there's a new/custom feature interface that needs to be added. + public IDictionary<string, FeatureMap> FeatureMaps + { + get { return _entries; } + } + + void IDictionary<string, object>.Add(string key, object value) + { + if (_entries.ContainsKey(key)) + { + throw new InvalidOperationException("Key already present"); + } + _context.Items.Add(key, value); + } + + bool IDictionary<string, object>.ContainsKey(string key) + { + object value; + return ((IDictionary<string, object>)this).TryGetValue(key, out value); + } + + ICollection<string> IDictionary<string, object>.Keys + { + get + { + object value; + return _entries.Where(pair => pair.Value.TryGet(_context, out value)) + .Select(pair => pair.Key).Concat(_context.Items.Keys.Select(key => Convert.ToString(key))).ToList(); + } + } + + bool IDictionary<string, object>.Remove(string key) + { + if (_entries.Remove(key)) + { + return true; + } + return _context.Items.Remove(key); + } + + bool IDictionary<string, object>.TryGetValue(string key, out object value) + { + FeatureMap entry; + if (_entries.TryGetValue(key, out entry) && entry.TryGet(_context, out value)) + { + return true; + } + return _context.Items.TryGetValue(key, out value); + } + + ICollection<object> IDictionary<string, object>.Values + { + get { throw new NotImplementedException(); } + } + + object IDictionary<string, object>.this[string key] + { + get + { + FeatureMap entry; + object value; + if (_entries.TryGetValue(key, out entry) && entry.TryGet(_context, out value)) + { + return value; + } + if (_context.Items.TryGetValue(key, out value)) + { + return value; + } + throw new KeyNotFoundException(key); + } + set + { + FeatureMap entry; + if (_entries.TryGetValue(key, out entry)) + { + if (entry.CanSet) + { + entry.Set(_context, value); + } + else + { + _entries.Remove(key); + if (value != null) + { + _context.Items[key] = value; + } + } + } + else + { + if (value == null) + { + _context.Items.Remove(key); + } + else + { + _context.Items[key] = value; + } + } + } + } + + void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item) + { + throw new NotImplementedException(); + } + + void ICollection<KeyValuePair<string, object>>.Clear() + { + _entries.Clear(); + _context.Items.Clear(); + } + + bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item) + { + throw new NotImplementedException(); + } + + void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + int ICollection<KeyValuePair<string, object>>.Count + { + get { return _entries.Count + _context.Items.Count; } + } + + bool ICollection<KeyValuePair<string, object>>.IsReadOnly + { + get { return false; } + } + + bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item) + { + throw new NotImplementedException(); + } + + public IEnumerator<KeyValuePair<string, object>> GetEnumerator() + { + foreach (var entryPair in _entries) + { + object value; + if (entryPair.Value.TryGet(_context, out value)) + { + yield return new KeyValuePair<string, object>(entryPair.Key, value); + } + } + foreach (var entryPair in _context.Items) + { + yield return new KeyValuePair<string, object>(Convert.ToString(entryPair.Key), entryPair.Value); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public class FeatureMap + { + public FeatureMap(Type featureInterface, Func<object, object> getter) + : this(featureInterface, getter, defaultFactory: null) + { + } + public FeatureMap(Type featureInterface, Func<object, object> getter, Func<object> defaultFactory) + : this(featureInterface, getter, defaultFactory, setter: null) + { + } + + public FeatureMap(Type featureInterface, Func<object, object> getter, Action<object, object> setter) + : this(featureInterface, getter, defaultFactory: null, setter: setter) + { + } + + public FeatureMap(Type featureInterface, Func<object, object> getter, Func<object> defaultFactory, Action<object, object> setter) + : this(featureInterface, getter, defaultFactory, setter, featureFactory: null) + { + } + + public FeatureMap(Type featureInterface, Func<object, object> getter, Func<object> defaultFactory, Action<object, object> setter, Func<object> featureFactory) + { + FeatureInterface = featureInterface; + Getter = getter; + Setter = setter; + DefaultFactory = defaultFactory; + FeatureFactory = featureFactory; + } + + private Type FeatureInterface { get; set; } + private Func<object, object> Getter { get; set; } + private Action<object, object> Setter { get; set; } + private Func<object> DefaultFactory { get; set; } + private Func<object> FeatureFactory { get; set; } + + public bool CanSet + { + get { return Setter != null; } + } + + internal bool TryGet(HttpContext context, out object value) + { + object featureInstance = context.Features[FeatureInterface]; + if (featureInstance == null) + { + value = null; + return false; + } + value = Getter(featureInstance); + if (value == null && DefaultFactory != null) + { + value = DefaultFactory(); + } + return true; + } + + internal void Set(HttpContext context, object value) + { + var feature = context.Features[FeatureInterface]; + if (feature == null) + { + if (FeatureFactory == null) + { + throw new InvalidOperationException("Missing feature: " + FeatureInterface.FullName); // TODO: LOC + } + else + { + feature = FeatureFactory(); + context.Features[FeatureInterface] = feature; + } + } + Setter(feature, value); + } + } + + public class FeatureMap<TFeature> : FeatureMap + { + public FeatureMap(Func<TFeature, object> getter) + : base(typeof(TFeature), feature => getter((TFeature)feature)) + { + } + + public FeatureMap(Func<TFeature, object> getter, Func<object> defaultFactory) + : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory) + { + } + + public FeatureMap(Func<TFeature, object> getter, Action<TFeature, object> setter) + : base(typeof(TFeature), feature => getter((TFeature)feature), (feature, value) => setter((TFeature)feature, value)) + { + } + + public FeatureMap(Func<TFeature, object> getter, Func<object> defaultFactory, Action<TFeature, object> setter) + : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory, (feature, value) => setter((TFeature)feature, value)) + { + } + + public FeatureMap(Func<TFeature, object> getter, Func<object> defaultFactory, Action<TFeature, object> setter, Func<TFeature> featureFactory) + : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory, (feature, value) => setter((TFeature)feature, value), () => featureFactory()) + { + } + } + } +} diff --git a/src/Http/Owin/src/OwinEnvironmentFeature.cs b/src/Http/Owin/src/OwinEnvironmentFeature.cs new file mode 100644 index 0000000000000000000000000000000000000000..14eb312608135c671a5d3addb7c3c614e439d14a --- /dev/null +++ b/src/Http/Owin/src/OwinEnvironmentFeature.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Owin +{ + public class OwinEnvironmentFeature : IOwinEnvironmentFeature + { + public IDictionary<string, object> Environment { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Owin/src/OwinExtensions.cs b/src/Http/Owin/src/OwinExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..0344c1a55253dc20b581235da04900dde038ffe8 --- /dev/null +++ b/src/Http/Owin/src/OwinExtensions.cs @@ -0,0 +1,175 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Owin; + +namespace Microsoft.AspNetCore.Builder +{ + using AddMiddleware = Action<Func< + Func<IDictionary<string, object>, Task>, + Func<IDictionary<string, object>, Task> + >>; + using AppFunc = Func<IDictionary<string, object>, Task>; + using CreateMiddleware = Func< + Func<IDictionary<string, object>, Task>, + Func<IDictionary<string, object>, Task> + >; + + public static class OwinExtensions + { + public static AddMiddleware UseOwin(this IApplicationBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + AddMiddleware add = middleware => + { + Func<RequestDelegate, RequestDelegate> middleware1 = next1 => + { + AppFunc exitMiddlware = env => + { + return next1((HttpContext)env[typeof(HttpContext).FullName]); + }; + var app = middleware(exitMiddlware); + return httpContext => + { + // Use the existing OWIN env if there is one. + IDictionary<string, object> env; + var owinEnvFeature = httpContext.Features.Get<IOwinEnvironmentFeature>(); + if (owinEnvFeature != null) + { + env = owinEnvFeature.Environment; + env[typeof(HttpContext).FullName] = httpContext; + } + else + { + env = new OwinEnvironment(httpContext); + } + return app.Invoke(env); + }; + }; + builder.Use(middleware1); + }; + // Adapt WebSockets by default. + add(WebSocketAcceptAdapter.AdaptWebSockets); + return add; + } + + public static IApplicationBuilder UseOwin(this IApplicationBuilder builder, Action<AddMiddleware> pipeline) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + if (pipeline == null) + { + throw new ArgumentNullException(nameof(pipeline)); + } + + pipeline(builder.UseOwin()); + return builder; + } + + public static IApplicationBuilder UseBuilder(this AddMiddleware app) + { + return app.UseBuilder(serviceProvider: null); + } + + public static IApplicationBuilder UseBuilder(this AddMiddleware app, IServiceProvider serviceProvider) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + // Do not set ApplicationBuilder.ApplicationServices to null. May fail later due to missing services but + // at least that results in a more useful Exception than a NRE. + if (serviceProvider == null) + { + serviceProvider = new EmptyProvider(); + } + + // Adapt WebSockets by default. + app(OwinWebSocketAcceptAdapter.AdaptWebSockets); + var builder = new ApplicationBuilder(serviceProvider: serviceProvider); + + var middleware = CreateMiddlewareFactory(exit => + { + builder.Use(ignored => exit); + return builder.Build(); + }, builder.ApplicationServices); + + app(middleware); + return builder; + } + + private static CreateMiddleware CreateMiddlewareFactory(Func<RequestDelegate, RequestDelegate> middleware, IServiceProvider services) + { + return next => + { + var app = middleware(httpContext => + { + return next(httpContext.Features.Get<IOwinEnvironmentFeature>().Environment); + }); + + return env => + { + // Use the existing HttpContext if there is one. + HttpContext context; + object obj; + if (env.TryGetValue(typeof(HttpContext).FullName, out obj)) + { + context = (HttpContext)obj; + context.Features.Set<IOwinEnvironmentFeature>(new OwinEnvironmentFeature() { Environment = env }); + } + else + { + context = new DefaultHttpContext( + new FeatureCollection( + new OwinFeatureCollection(env))); + context.RequestServices = services; + } + + return app.Invoke(context); + }; + }; + } + + public static AddMiddleware UseBuilder(this AddMiddleware app, Action<IApplicationBuilder> pipeline) + { + return app.UseBuilder(pipeline, serviceProvider: null); + } + + public static AddMiddleware UseBuilder(this AddMiddleware app, Action<IApplicationBuilder> pipeline, IServiceProvider serviceProvider) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + if (pipeline == null) + { + throw new ArgumentNullException(nameof(pipeline)); + } + + var builder = app.UseBuilder(serviceProvider); + pipeline(builder); + return app; + } + + private class EmptyProvider : IServiceProvider + { + public object GetService(Type serviceType) + { + return null; + } + } + } +} diff --git a/src/Http/Owin/src/OwinFeatureCollection.cs b/src/Http/Owin/src/OwinFeatureCollection.cs new file mode 100644 index 0000000000000000000000000000000000000000..4838b99f5cd0bc73e546e10b60a075a8db8cf099 --- /dev/null +++ b/src/Http/Owin/src/OwinFeatureCollection.cs @@ -0,0 +1,412 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.WebSockets; +using System.Reflection; +using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; + +namespace Microsoft.AspNetCore.Owin +{ + using SendFileFunc = Func<string, long, long?, CancellationToken, Task>; + + public class OwinFeatureCollection : + IFeatureCollection, + IHttpRequestFeature, + IHttpResponseFeature, + IHttpConnectionFeature, + IHttpSendFileFeature, + ITlsConnectionFeature, + IHttpRequestIdentifierFeature, + IHttpRequestLifetimeFeature, + IHttpAuthenticationFeature, + IHttpWebSocketFeature, + IOwinEnvironmentFeature + { + public IDictionary<string, object> Environment { get; set; } + private bool _headersSent; + + public OwinFeatureCollection(IDictionary<string, object> environment) + { + Environment = environment; + SupportsWebSockets = true; + + var register = Prop<Action<Action<object>, object>>(OwinConstants.CommonKeys.OnSendingHeaders); + register?.Invoke(state => + { + var collection = (OwinFeatureCollection)state; + collection._headersSent = true; + }, this); + } + + T Prop<T>(string key) + { + object value; + if (Environment.TryGetValue(key, out value) && value is T) + { + return (T)value; + } + return default(T); + } + + void Prop(string key, object value) + { + Environment[key] = value; + } + + string IHttpRequestFeature.Protocol + { + get { return Prop<string>(OwinConstants.RequestProtocol); } + set { Prop(OwinConstants.RequestProtocol, value); } + } + + string IHttpRequestFeature.Scheme + { + get { return Prop<string>(OwinConstants.RequestScheme); } + set { Prop(OwinConstants.RequestScheme, value); } + } + + string IHttpRequestFeature.Method + { + get { return Prop<string>(OwinConstants.RequestMethod); } + set { Prop(OwinConstants.RequestMethod, value); } + } + + string IHttpRequestFeature.PathBase + { + get { return Prop<string>(OwinConstants.RequestPathBase); } + set { Prop(OwinConstants.RequestPathBase, value); } + } + + string IHttpRequestFeature.Path + { + get { return Prop<string>(OwinConstants.RequestPath); } + set { Prop(OwinConstants.RequestPath, value); } + } + + string IHttpRequestFeature.QueryString + { + get { return Utilities.AddQuestionMark(Prop<string>(OwinConstants.RequestQueryString)); } + set { Prop(OwinConstants.RequestQueryString, Utilities.RemoveQuestionMark(value)); } + } + + string IHttpRequestFeature.RawTarget + { + get { return string.Empty; } + set { throw new NotSupportedException(); } + } + + IHeaderDictionary IHttpRequestFeature.Headers + { + get { return Utilities.MakeHeaderDictionary(Prop<IDictionary<string, string[]>>(OwinConstants.RequestHeaders)); } + set { Prop(OwinConstants.RequestHeaders, Utilities.MakeDictionaryStringArray(value)); } + } + + string IHttpRequestIdentifierFeature.TraceIdentifier + { + get { return Prop<string>(OwinConstants.RequestId); } + set { Prop(OwinConstants.RequestId, value); } + } + + Stream IHttpRequestFeature.Body + { + get { return Prop<Stream>(OwinConstants.RequestBody); } + set { Prop(OwinConstants.RequestBody, value); } + } + + int IHttpResponseFeature.StatusCode + { + get { return Prop<int>(OwinConstants.ResponseStatusCode); } + set { Prop(OwinConstants.ResponseStatusCode, value); } + } + + string IHttpResponseFeature.ReasonPhrase + { + get { return Prop<string>(OwinConstants.ResponseReasonPhrase); } + set { Prop(OwinConstants.ResponseReasonPhrase, value); } + } + + IHeaderDictionary IHttpResponseFeature.Headers + { + get { return Utilities.MakeHeaderDictionary(Prop<IDictionary<string, string[]>>(OwinConstants.ResponseHeaders)); } + set { Prop(OwinConstants.ResponseHeaders, Utilities.MakeDictionaryStringArray(value)); } + } + + Stream IHttpResponseFeature.Body + { + get { return Prop<Stream>(OwinConstants.ResponseBody); } + set { Prop(OwinConstants.ResponseBody, value); } + } + + bool IHttpResponseFeature.HasStarted + { + get { return _headersSent; } + } + + void IHttpResponseFeature.OnStarting(Func<object, Task> callback, object state) + { + var register = Prop<Action<Action<object>, object>>(OwinConstants.CommonKeys.OnSendingHeaders); + if (register == null) + { + throw new NotSupportedException(OwinConstants.CommonKeys.OnSendingHeaders); + } + + // Need to block on the callback since we can't change the OWIN signature to be async + register(s => callback(s).GetAwaiter().GetResult(), state); + } + + void IHttpResponseFeature.OnCompleted(Func<object, Task> callback, object state) + { + throw new NotSupportedException(); + } + + IPAddress IHttpConnectionFeature.RemoteIpAddress + { + get { return IPAddress.Parse(Prop<string>(OwinConstants.CommonKeys.RemoteIpAddress)); } + set { Prop(OwinConstants.CommonKeys.RemoteIpAddress, value.ToString()); } + } + + IPAddress IHttpConnectionFeature.LocalIpAddress + { + get { return IPAddress.Parse(Prop<string>(OwinConstants.CommonKeys.LocalIpAddress)); } + set { Prop(OwinConstants.CommonKeys.LocalIpAddress, value.ToString()); } + } + + int IHttpConnectionFeature.RemotePort + { + get { return int.Parse(Prop<string>(OwinConstants.CommonKeys.RemotePort)); } + set { Prop(OwinConstants.CommonKeys.RemotePort, value.ToString(CultureInfo.InvariantCulture)); } + } + + int IHttpConnectionFeature.LocalPort + { + get { return int.Parse(Prop<string>(OwinConstants.CommonKeys.LocalPort)); } + set { Prop(OwinConstants.CommonKeys.LocalPort, value.ToString(CultureInfo.InvariantCulture)); } + } + + string IHttpConnectionFeature.ConnectionId + { + get { return Prop<string>(OwinConstants.CommonKeys.ConnectionId); } + set { Prop(OwinConstants.CommonKeys.ConnectionId, value); } + } + + private bool SupportsSendFile + { + get + { + object obj; + return Environment.TryGetValue(OwinConstants.SendFiles.SendAsync, out obj) && obj != null; + } + } + + Task IHttpSendFileFeature.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) + { + object obj; + if (Environment.TryGetValue(OwinConstants.SendFiles.SendAsync, out obj)) + { + var func = (SendFileFunc)obj; + return func(path, offset, length, cancellation); + } + throw new NotSupportedException(OwinConstants.SendFiles.SendAsync); + } + + private bool SupportsClientCerts + { + get + { + object obj; + if (string.Equals("https", ((IHttpRequestFeature)this).Scheme, StringComparison.OrdinalIgnoreCase) + && (Environment.TryGetValue(OwinConstants.CommonKeys.LoadClientCertAsync, out obj) + || Environment.TryGetValue(OwinConstants.CommonKeys.ClientCertificate, out obj)) + && obj != null) + { + return true; + } + return false; + } + } + + X509Certificate2 ITlsConnectionFeature.ClientCertificate + { + get { return Prop<X509Certificate2>(OwinConstants.CommonKeys.ClientCertificate); } + set { Prop(OwinConstants.CommonKeys.ClientCertificate, value); } + } + + async Task<X509Certificate2> ITlsConnectionFeature.GetClientCertificateAsync(CancellationToken cancellationToken) + { + var loadAsync = Prop<Func<Task>>(OwinConstants.CommonKeys.LoadClientCertAsync); + if (loadAsync != null) + { + await loadAsync(); + } + return Prop<X509Certificate2>(OwinConstants.CommonKeys.ClientCertificate); + } + + CancellationToken IHttpRequestLifetimeFeature.RequestAborted + { + get { return Prop<CancellationToken>(OwinConstants.CallCancelled); } + set { Prop(OwinConstants.CallCancelled, value); } + } + + void IHttpRequestLifetimeFeature.Abort() + { + throw new NotImplementedException(); + } + + ClaimsPrincipal IHttpAuthenticationFeature.User + { + get + { + return Prop<ClaimsPrincipal>(OwinConstants.RequestUser) + ?? Utilities.MakeClaimsPrincipal(Prop<IPrincipal>(OwinConstants.Security.User)); + } + set + { + Prop(OwinConstants.RequestUser, value); + Prop(OwinConstants.Security.User, value); + } + } + + IAuthenticationHandler IHttpAuthenticationFeature.Handler { get; set; } + + /// <summary> + /// Gets or sets if the underlying server supports WebSockets. This is enabled by default. + /// The value should be consistent across requests. + /// </summary> + public bool SupportsWebSockets { get; set; } + + bool IHttpWebSocketFeature.IsWebSocketRequest + { + get + { + object obj; + return Environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out obj); + } + } + + Task<WebSocket> IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context) + { + object obj; + if (!Environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out obj)) + { + throw new NotSupportedException("WebSockets are not supported"); // TODO: LOC + } + var accept = (Func<WebSocketAcceptContext, Task<WebSocket>>)obj; + return accept(context); + } + + // IFeatureCollection + + public int Revision + { + get { return 0; } // Not modifiable + } + + public bool IsReadOnly + { + get { return true; } + } + + public object this[Type key] + { + get { return Get(key); } + set { throw new NotSupportedException(); } + } + + private bool SupportsInterface(Type key) + { + // Does this type implement the requested interface? + if (key.GetTypeInfo().IsAssignableFrom(GetType().GetTypeInfo())) + { + // Check for conditional features + if (key == typeof(IHttpSendFileFeature)) + { + return SupportsSendFile; + } + else if (key == typeof(ITlsConnectionFeature)) + { + return SupportsClientCerts; + } + else if (key == typeof(IHttpWebSocketFeature)) + { + return SupportsWebSockets; + } + + // The rest of the features are always supported. + return true; + } + return false; + } + + public object Get(Type key) + { + if (SupportsInterface(key)) + { + return this; + } + return null; + } + + public void Set(Type key, object value) + { + throw new NotSupportedException(); + } + + public TFeature Get<TFeature>() + { + return (TFeature)this[typeof(TFeature)]; + } + + public void Set<TFeature>(TFeature instance) + { + this[typeof(TFeature)] = instance; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IEnumerator<KeyValuePair<Type, object>> GetEnumerator() + { + yield return new KeyValuePair<Type, object>(typeof(IHttpRequestFeature), this); + yield return new KeyValuePair<Type, object>(typeof(IHttpResponseFeature), this); + yield return new KeyValuePair<Type, object>(typeof(IHttpConnectionFeature), this); + yield return new KeyValuePair<Type, object>(typeof(IHttpRequestIdentifierFeature), this); + yield return new KeyValuePair<Type, object>(typeof(IHttpRequestLifetimeFeature), this); + yield return new KeyValuePair<Type, object>(typeof(IHttpAuthenticationFeature), this); + yield return new KeyValuePair<Type, object>(typeof(IOwinEnvironmentFeature), this); + + // Check for conditional features + if (SupportsSendFile) + { + yield return new KeyValuePair<Type, object>(typeof(IHttpSendFileFeature), this); + } + if (SupportsClientCerts) + { + yield return new KeyValuePair<Type, object>(typeof(ITlsConnectionFeature), this); + } + if (SupportsWebSockets) + { + yield return new KeyValuePair<Type, object>(typeof(IHttpWebSocketFeature), this); + } + } + + public void Dispose() + { + } + } +} + diff --git a/src/Http/Owin/src/Utilities.cs b/src/Http/Owin/src/Utilities.cs new file mode 100644 index 0000000000000000000000000000000000000000..b65cae78a9fca3d7b0fcd1d5da0c540ab0817d4e --- /dev/null +++ b/src/Http/Owin/src/Utilities.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Security.Principal; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Owin +{ + internal static class Utilities + { + internal static string RemoveQuestionMark(string queryString) + { + if (!string.IsNullOrEmpty(queryString)) + { + if (queryString[0] == '?') + { + return queryString.Substring(1); + } + } + return queryString; + } + + internal static string AddQuestionMark(string queryString) + { + if (!string.IsNullOrEmpty(queryString)) + { + return '?' + queryString; + } + return queryString; + } + + internal static ClaimsPrincipal MakeClaimsPrincipal(IPrincipal principal) + { + if (principal == null) + { + return null; + } + if (principal is ClaimsPrincipal) + { + return principal as ClaimsPrincipal; + } + return new ClaimsPrincipal(principal); + } + + internal static IHeaderDictionary MakeHeaderDictionary(IDictionary<string, string[]> dictionary) + { + var wrapper = dictionary as DictionaryStringArrayWrapper; + if (wrapper != null) + { + return wrapper.Inner; + } + return new DictionaryStringValuesWrapper(dictionary); + } + + internal static IDictionary<string, string[]> MakeDictionaryStringArray(IHeaderDictionary dictionary) + { + var wrapper = dictionary as DictionaryStringValuesWrapper; + if (wrapper != null) + { + return wrapper.Inner; + } + return new DictionaryStringArrayWrapper(dictionary); + } + } +} diff --git a/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptAdapter.cs b/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptAdapter.cs new file mode 100644 index 0000000000000000000000000000000000000000..5fe43dedd288edf4284a1a1ee7ce85eb0fa1393c --- /dev/null +++ b/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptAdapter.cs @@ -0,0 +1,143 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Owin +{ + using AppFunc = Func<IDictionary<string, object>, Task>; + using WebSocketAccept = + Action + < + IDictionary<string, object>, // WebSocket Accept parameters + Func // WebSocketFunc callback + < + IDictionary<string, object>, // WebSocket environment + Task // Complete + > + >; + using WebSocketAcceptAlt = + Func + < + WebSocketAcceptContext, // WebSocket Accept parameters + Task<WebSocket> + >; + + /// <summary> + /// This adapts the OWIN WebSocket accept flow to match the ASP.NET Core WebSocket Accept flow. + /// This enables ASP.NET Core components to use WebSockets on OWIN based servers. + /// </summary> + public class OwinWebSocketAcceptAdapter + { + private WebSocketAccept _owinWebSocketAccept; + private TaskCompletionSource<int> _requestTcs = new TaskCompletionSource<int>(); + private TaskCompletionSource<WebSocket> _acceptTcs = new TaskCompletionSource<WebSocket>(); + private TaskCompletionSource<int> _upstreamWentAsync = new TaskCompletionSource<int>(); + private string _subProtocol = null; + + private OwinWebSocketAcceptAdapter(WebSocketAccept owinWebSocketAccept) + { + _owinWebSocketAccept = owinWebSocketAccept; + } + + private Task RequestTask { get { return _requestTcs.Task; } } + private Task UpstreamTask { get; set; } + private TaskCompletionSource<int> UpstreamWentAsyncTcs { get { return _upstreamWentAsync; } } + + private async Task<WebSocket> AcceptWebSocketAsync(WebSocketAcceptContext context) + { + IDictionary<string, object> options = null; + if (context is OwinWebSocketAcceptContext) + { + var acceptContext = context as OwinWebSocketAcceptContext; + options = acceptContext.Options; + _subProtocol = acceptContext.SubProtocol; + } + else if (context?.SubProtocol != null) + { + options = new Dictionary<string, object>(1) + { + { OwinConstants.WebSocket.SubProtocol, context.SubProtocol } + }; + _subProtocol = context.SubProtocol; + } + + // Accept may have been called synchronously on the original request thread, we might not have a task yet. Go async. + await _upstreamWentAsync.Task; + + _owinWebSocketAccept(options, OwinAcceptCallback); + _requestTcs.TrySetResult(0); // Let the pipeline unwind. + + return await _acceptTcs.Task; + } + + private Task OwinAcceptCallback(IDictionary<string, object> webSocketContext) + { + _acceptTcs.TrySetResult(new OwinWebSocketAdapter(webSocketContext, _subProtocol)); + return UpstreamTask; + } + + // Make sure declined websocket requests complete. This is a no-op for accepted websocket requests. + private void EnsureCompleted(Task task) + { + if (task.IsCanceled) + { + _requestTcs.TrySetCanceled(); + } + else if (task.IsFaulted) + { + _requestTcs.TrySetException(task.Exception); + } + else + { + _requestTcs.TrySetResult(0); + } + } + + // Order of operations: + // 1. A WebSocket handshake request is received by the middleware. + // 2. The middleware inserts an alternate Accept signature into the OWIN environment. + // 3. The middleware invokes Next and stores Next's Task locally. It then returns an alternate Task to the server. + // 4. The OwinFeatureCollection adapts the alternate Accept signature to IHttpWebSocketFeature.AcceptAsync. + // 5. A component later in the pipleline invokes IHttpWebSocketFeature.AcceptAsync (mapped to AcceptWebSocketAsync). + // 6. The middleware calls the OWIN Accept, providing a local callback, and returns an incomplete Task. + // 7. The middleware completes the alternate Task it returned from Invoke, telling the server that the request pipeline has completed. + // 8. The server invokes the middleware's callback, which creats a WebSocket adapter complete's the orriginal Accept Task with it. + // 9. The middleware waits while the application uses the WebSocket, where the end is signaled by the Next's Task completion. + public static AppFunc AdaptWebSockets(AppFunc next) + { + return environment => + { + object accept; + if (environment.TryGetValue(OwinConstants.WebSocket.Accept, out accept) && accept is WebSocketAccept) + { + var adapter = new OwinWebSocketAcceptAdapter((WebSocketAccept)accept); + + environment[OwinConstants.WebSocket.AcceptAlt] = new WebSocketAcceptAlt(adapter.AcceptWebSocketAsync); + + try + { + adapter.UpstreamTask = next(environment); + adapter.UpstreamWentAsyncTcs.TrySetResult(0); + adapter.UpstreamTask.ContinueWith(adapter.EnsureCompleted, TaskContinuationOptions.ExecuteSynchronously); + } + catch (Exception ex) + { + adapter.UpstreamWentAsyncTcs.TrySetException(ex); + throw; + } + + return adapter.RequestTask; + } + else + { + return next(environment); + } + }; + } + } +} \ No newline at end of file diff --git a/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptContext.cs b/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..a9fd28edbae8c81f3b3a6cb06f588927f7b7ca2e --- /dev/null +++ b/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptContext.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Owin +{ + public class OwinWebSocketAcceptContext : WebSocketAcceptContext + { + private IDictionary<string, object> _options; + + public OwinWebSocketAcceptContext() : this(new Dictionary<string, object>(1)) + { + } + + public OwinWebSocketAcceptContext(IDictionary<string, object> options) + { + _options = options; + } + + public override string SubProtocol + { + get + { + object obj; + if (_options != null && _options.TryGetValue(OwinConstants.WebSocket.SubProtocol, out obj)) + { + return (string)obj; + } + return null; + } + set + { + if (_options == null) + { + _options = new Dictionary<string, object>(1); + } + _options[OwinConstants.WebSocket.SubProtocol] = value; + } + } + + public IDictionary<string, object> Options + { + get { return _options; } + } + } +} \ No newline at end of file diff --git a/src/Http/Owin/src/WebSockets/OwinWebSocketAdapter.cs b/src/Http/Owin/src/WebSockets/OwinWebSocketAdapter.cs new file mode 100644 index 0000000000000000000000000000000000000000..e7eed159ea370032e950199a1df732b0b806d370 --- /dev/null +++ b/src/Http/Owin/src/WebSockets/OwinWebSocketAdapter.cs @@ -0,0 +1,200 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Net.WebSockets; + +namespace Microsoft.AspNetCore.Owin +{ + // http://owin.org/extensions/owin-WebSocket-Extension-v0.4.0.htm + using WebSocketCloseAsync = + Func<int /* closeStatus */, + string /* closeDescription */, + CancellationToken /* cancel */, + Task>; + using WebSocketReceiveAsync = + Func<ArraySegment<byte> /* data */, + CancellationToken /* cancel */, + Task<Tuple<int /* messageType */, + bool /* endOfMessage */, + int /* count */>>>; + using WebSocketSendAsync = + Func<ArraySegment<byte> /* data */, + int /* messageType */, + bool /* endOfMessage */, + CancellationToken /* cancel */, + Task>; + using RawWebSocketReceiveResult = Tuple<int, // type + bool, // end of message? + int>; // count + + public class OwinWebSocketAdapter : WebSocket + { + private const int _rentedBufferSize = 1024; + private IDictionary<string, object> _websocketContext; + private WebSocketSendAsync _sendAsync; + private WebSocketReceiveAsync _receiveAsync; + private WebSocketCloseAsync _closeAsync; + private WebSocketState _state; + private string _subProtocol; + + public OwinWebSocketAdapter(IDictionary<string, object> websocketContext, string subProtocol) + { + _websocketContext = websocketContext; + _sendAsync = (WebSocketSendAsync)websocketContext[OwinConstants.WebSocket.SendAsync]; + _receiveAsync = (WebSocketReceiveAsync)websocketContext[OwinConstants.WebSocket.ReceiveAsync]; + _closeAsync = (WebSocketCloseAsync)websocketContext[OwinConstants.WebSocket.CloseAsync]; + _state = WebSocketState.Open; + _subProtocol = subProtocol; + } + + public override WebSocketCloseStatus? CloseStatus + { + get + { + object obj; + if (_websocketContext.TryGetValue(OwinConstants.WebSocket.ClientCloseStatus, out obj)) + { + return (WebSocketCloseStatus)obj; + } + return null; + } + } + + public override string CloseStatusDescription + { + get + { + object obj; + if (_websocketContext.TryGetValue(OwinConstants.WebSocket.ClientCloseDescription, out obj)) + { + return (string)obj; + } + return null; + } + } + + public override string SubProtocol + { + get + { + return _subProtocol; + } + } + + public override WebSocketState State + { + get + { + return _state; + } + } + + public override async Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken) + { + var rawResult = await _receiveAsync(buffer, cancellationToken); + var messageType = OpCodeToEnum(rawResult.Item1); + if (messageType == WebSocketMessageType.Close) + { + if (State == WebSocketState.Open) + { + _state = WebSocketState.CloseReceived; + } + else if (State == WebSocketState.CloseSent) + { + _state = WebSocketState.Closed; + } + return new WebSocketReceiveResult(rawResult.Item3, messageType, rawResult.Item2, CloseStatus, CloseStatusDescription); + } + else + { + return new WebSocketReceiveResult(rawResult.Item3, messageType, rawResult.Item2); + } + } + + public override Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + { + return _sendAsync(buffer, EnumToOpCode(messageType), endOfMessage, cancellationToken); + } + + public override async Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + if (State == WebSocketState.Open || State == WebSocketState.CloseReceived) + { + await CloseOutputAsync(closeStatus, statusDescription, cancellationToken); + } + + var buffer = ArrayPool<byte>.Shared.Rent(_rentedBufferSize); + try + { + while (State == WebSocketState.CloseSent) + { + // Drain until close received + await ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken); + } + } + finally + { + ArrayPool<byte>.Shared.Return(buffer); + } + } + + public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + // TODO: Validate state + if (State == WebSocketState.Open) + { + _state = WebSocketState.CloseSent; + } + else if (State == WebSocketState.CloseReceived) + { + _state = WebSocketState.Closed; + } + return _closeAsync((int)closeStatus, statusDescription, cancellationToken); + } + + public override void Abort() + { + _state = WebSocketState.Aborted; + } + + public override void Dispose() + { + _state = WebSocketState.Closed; + } + + private static WebSocketMessageType OpCodeToEnum(int messageType) + { + switch (messageType) + { + case 0x1: + return WebSocketMessageType.Text; + case 0x2: + return WebSocketMessageType.Binary; + case 0x8: + return WebSocketMessageType.Close; + default: + throw new ArgumentOutOfRangeException(nameof(messageType), messageType, string.Empty); + } + } + + private static int EnumToOpCode(WebSocketMessageType webSocketMessageType) + { + switch (webSocketMessageType) + { + case WebSocketMessageType.Text: + return 0x1; + case WebSocketMessageType.Binary: + return 0x2; + case WebSocketMessageType.Close: + return 0x8; + default: + throw new ArgumentOutOfRangeException(nameof(webSocketMessageType), webSocketMessageType, string.Empty); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Owin/src/WebSockets/WebSocketAcceptAdapter.cs b/src/Http/Owin/src/WebSockets/WebSocketAcceptAdapter.cs new file mode 100644 index 0000000000000000000000000000000000000000..f1355da4c247449eef621a2279e7522e219662f5 --- /dev/null +++ b/src/Http/Owin/src/WebSockets/WebSocketAcceptAdapter.cs @@ -0,0 +1,92 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Owin +{ + using AppFunc = Func<IDictionary<string, object>, Task>; + using WebSocketAccept = + Action + < + IDictionary<string, object>, // WebSocket Accept parameters + Func // WebSocketFunc callback + < + IDictionary<string, object>, // WebSocket environment + Task // Complete + > + >; + using WebSocketAcceptAlt = + Func + < + WebSocketAcceptContext, // WebSocket Accept parameters + Task<WebSocket> + >; + + /// <summary> + /// This adapts the ASP.NET Core WebSocket Accept flow to match the OWIN WebSocket accept flow. + /// This enables OWIN based components to use WebSockets on ASP.NET Core servers. + /// </summary> + public class WebSocketAcceptAdapter + { + private IDictionary<string, object> _env; + private WebSocketAcceptAlt _accept; + private AppFunc _callback; + private IDictionary<string, object> _options; + + public WebSocketAcceptAdapter(IDictionary<string, object> env, WebSocketAcceptAlt accept) + { + _env = env; + _accept = accept; + } + + private void AcceptWebSocket(IDictionary<string, object> options, AppFunc callback) + { + _options = options; + _callback = callback; + _env[OwinConstants.ResponseStatusCode] = 101; + } + + public static AppFunc AdaptWebSockets(AppFunc next) + { + return async environment => + { + object accept; + if (environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out accept) && accept is WebSocketAcceptAlt) + { + var adapter = new WebSocketAcceptAdapter(environment, (WebSocketAcceptAlt)accept); + + environment[OwinConstants.WebSocket.Accept] = new WebSocketAccept(adapter.AcceptWebSocket); + await next(environment); + if ((int)environment[OwinConstants.ResponseStatusCode] == 101 && adapter._callback != null) + { + WebSocketAcceptContext acceptContext = null; + object obj; + if (adapter._options != null && adapter._options.TryGetValue(typeof(WebSocketAcceptContext).FullName, out obj)) + { + acceptContext = obj as WebSocketAcceptContext; + } + else if (adapter._options != null) + { + acceptContext = new OwinWebSocketAcceptContext(adapter._options); + } + + var webSocket = await adapter._accept(acceptContext); + var webSocketAdapter = new WebSocketAdapter(webSocket, (CancellationToken)environment[OwinConstants.CallCancelled]); + await adapter._callback(webSocketAdapter.Environment); + await webSocketAdapter.CleanupAsync(); + } + } + else + { + await next(environment); + } + }; + } + } +} \ No newline at end of file diff --git a/src/Http/Owin/src/WebSockets/WebSocketAdapter.cs b/src/Http/Owin/src/WebSockets/WebSocketAdapter.cs new file mode 100644 index 0000000000000000000000000000000000000000..7fad98704cfee01f0d194893d8e90fc4955915a5 --- /dev/null +++ b/src/Http/Owin/src/WebSockets/WebSocketAdapter.cs @@ -0,0 +1,171 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Owin +{ + using WebSocketCloseAsync = + Func<int /* closeStatus */, + string /* closeDescription */, + CancellationToken /* cancel */, + Task>; + using WebSocketReceiveAsync = + Func<ArraySegment<byte> /* data */, + CancellationToken /* cancel */, + Task<Tuple<int /* messageType */, + bool /* endOfMessage */, + int /* count */>>>; + using WebSocketReceiveTuple = + Tuple<int /* messageType */, + bool /* endOfMessage */, + int /* count */>; + using WebSocketSendAsync = + Func<ArraySegment<byte> /* data */, + int /* messageType */, + bool /* endOfMessage */, + CancellationToken /* cancel */, + Task>; + + public class WebSocketAdapter + { + private readonly WebSocket _webSocket; + private readonly IDictionary<string, object> _environment; + private readonly CancellationToken _cancellationToken; + + internal WebSocketAdapter(WebSocket webSocket, CancellationToken ct) + { + _webSocket = webSocket; + _cancellationToken = ct; + + _environment = new Dictionary<string, object>(); + _environment[OwinConstants.WebSocket.SendAsync] = new WebSocketSendAsync(SendAsync); + _environment[OwinConstants.WebSocket.ReceiveAsync] = new WebSocketReceiveAsync(ReceiveAsync); + _environment[OwinConstants.WebSocket.CloseAsync] = new WebSocketCloseAsync(CloseAsync); + _environment[OwinConstants.WebSocket.CallCancelled] = ct; + _environment[OwinConstants.WebSocket.Version] = OwinConstants.WebSocket.VersionValue; + + _environment[typeof(WebSocket).FullName] = webSocket; + } + + internal IDictionary<string, object> Environment + { + get { return _environment; } + } + + internal Task SendAsync(ArraySegment<byte> buffer, int messageType, bool endOfMessage, CancellationToken cancel) + { + // Remap close messages to CloseAsync. System.Net.WebSockets.WebSocket.SendAsync does not allow close messages. + if (messageType == 0x8) + { + return RedirectSendToCloseAsync(buffer, cancel); + } + else if (messageType == 0x9 || messageType == 0xA) + { + // Ping & Pong, not allowed by the underlying APIs, silently discard. + return Task.CompletedTask; + } + + return _webSocket.SendAsync(buffer, OpCodeToEnum(messageType), endOfMessage, cancel); + } + + internal async Task<WebSocketReceiveTuple> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancel) + { + WebSocketReceiveResult nativeResult = await _webSocket.ReceiveAsync(buffer, cancel); + + if (nativeResult.MessageType == WebSocketMessageType.Close) + { + _environment[OwinConstants.WebSocket.ClientCloseStatus] = (int)(nativeResult.CloseStatus ?? WebSocketCloseStatus.NormalClosure); + _environment[OwinConstants.WebSocket.ClientCloseDescription] = nativeResult.CloseStatusDescription ?? string.Empty; + } + + return new WebSocketReceiveTuple( + EnumToOpCode(nativeResult.MessageType), + nativeResult.EndOfMessage, + nativeResult.Count); + } + + internal Task CloseAsync(int status, string description, CancellationToken cancel) + { + return _webSocket.CloseOutputAsync((WebSocketCloseStatus)status, description, cancel); + } + + private Task RedirectSendToCloseAsync(ArraySegment<byte> buffer, CancellationToken cancel) + { + if (buffer.Array == null || buffer.Count == 0) + { + return CloseAsync(1000, string.Empty, cancel); + } + else if (buffer.Count >= 2) + { + // Unpack the close message. + int statusCode = + (buffer.Array[buffer.Offset] << 8) + | buffer.Array[buffer.Offset + 1]; + string description = Encoding.UTF8.GetString(buffer.Array, buffer.Offset + 2, buffer.Count - 2); + + return CloseAsync(statusCode, description, cancel); + } + else + { + throw new ArgumentOutOfRangeException(nameof(buffer)); + } + } + + internal async Task CleanupAsync() + { + switch (_webSocket.State) + { + case WebSocketState.Closed: // Closed gracefully, no action needed. + case WebSocketState.Aborted: // Closed abortively, no action needed. + break; + case WebSocketState.CloseReceived: + // Echo what the client said, if anything. + await _webSocket.CloseAsync(_webSocket.CloseStatus ?? WebSocketCloseStatus.NormalClosure, + _webSocket.CloseStatusDescription ?? string.Empty, _cancellationToken); + break; + case WebSocketState.Open: + case WebSocketState.CloseSent: // No close received, abort so we don't have to drain the pipe. + _webSocket.Abort(); + break; + default: + throw new NotSupportedException($"Unsupported {nameof(WebSocketState)} value: {_webSocket.State}."); + } + } + + private static WebSocketMessageType OpCodeToEnum(int messageType) + { + switch (messageType) + { + case 0x1: + return WebSocketMessageType.Text; + case 0x2: + return WebSocketMessageType.Binary; + case 0x8: + return WebSocketMessageType.Close; + default: + throw new ArgumentOutOfRangeException(nameof(messageType), messageType, string.Empty); + } + } + + private static int EnumToOpCode(WebSocketMessageType webSocketMessageType) + { + switch (webSocketMessageType) + { + case WebSocketMessageType.Text: + return 0x1; + case WebSocketMessageType.Binary: + return 0x2; + case WebSocketMessageType.Close: + return 0x8; + default: + throw new ArgumentOutOfRangeException(nameof(webSocketMessageType), webSocketMessageType, string.Empty); + } + } + } +} diff --git a/src/Http/Owin/src/baseline.netcore.json b/src/Http/Owin/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..8211307418048e28f73f4491d63c31b97fbe734d --- /dev/null +++ b/src/Http/Owin/src/baseline.netcore.json @@ -0,0 +1,1010 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Owin, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Builder.OwinExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseOwin", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "System.Action<System.Func<System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>, System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>>>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseOwin", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "pipeline", + "Type": "System.Action<System.Action<System.Func<System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>, System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>>>>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseBuilder", + "Parameters": [ + { + "Name": "app", + "Type": "System.Action<System.Func<System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>, System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>>>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseBuilder", + "Parameters": [ + { + "Name": "app", + "Type": "System.Action<System.Func<System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>, System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>>>" + }, + { + "Name": "serviceProvider", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseBuilder", + "Parameters": [ + { + "Name": "app", + "Type": "System.Action<System.Func<System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>, System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>>>" + }, + { + "Name": "pipeline", + "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>" + } + ], + "ReturnType": "System.Action<System.Func<System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>, System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>>>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseBuilder", + "Parameters": [ + { + "Name": "app", + "Type": "System.Action<System.Func<System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>, System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>>>" + }, + { + "Name": "pipeline", + "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>" + }, + { + "Name": "serviceProvider", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "System.Action<System.Func<System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>, System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>>>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Environment", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, System.Object>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Environment", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinEnvironment", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.Collections.Generic.IDictionary<System.String, System.Object>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<System.String, System.Object>>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.String, System.Object>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FeatureMaps", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, Microsoft.AspNetCore.Owin.OwinEnvironment+FeatureMap>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinEnvironmentFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Environment", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, System.Object>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Environment", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinFeatureCollection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature", + "Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature", + "Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature", + "Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature", + "Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature", + "Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature", + "Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<System.Type, System.Object>>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.Type, System.Object>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Environment", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, System.Object>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Environment", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SupportsWebSockets", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SupportsWebSockets", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Revision", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsReadOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + } + ], + "ReturnType": "System.Object", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Get", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + } + ], + "ReturnType": "System.Object", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Set", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Get<T0>", + "Parameters": [], + "ReturnType": "T0", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Set<T0>", + "Parameters": [ + { + "Name": "instance", + "Type": "T0" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "environment", + "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinWebSocketAcceptAdapter", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AdaptWebSockets", + "Parameters": [ + { + "Name": "next", + "Type": "System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinWebSocketAcceptContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Http.WebSocketAcceptContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_SubProtocol", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SubProtocol", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Options", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary<System.String, System.Object>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinWebSocketAdapter", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.Net.WebSockets.WebSocket", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "ImplementedInterface": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CloseStatus", + "Parameters": [], + "ReturnType": "System.Nullable<System.Net.WebSockets.WebSocketCloseStatus>", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CloseStatusDescription", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SubProtocol", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_State", + "Parameters": [], + "ReturnType": "System.Net.WebSockets.WebSocketState", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReceiveAsync", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.ArraySegment<System.Byte>" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Net.WebSockets.WebSocketReceiveResult>", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SendAsync", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.ArraySegment<System.Byte>" + }, + { + "Name": "messageType", + "Type": "System.Net.WebSockets.WebSocketMessageType" + }, + { + "Name": "endOfMessage", + "Type": "System.Boolean" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CloseAsync", + "Parameters": [ + { + "Name": "closeStatus", + "Type": "System.Net.WebSockets.WebSocketCloseStatus" + }, + { + "Name": "statusDescription", + "Type": "System.String" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CloseOutputAsync", + "Parameters": [ + { + "Name": "closeStatus", + "Type": "System.Net.WebSockets.WebSocketCloseStatus" + }, + { + "Name": "statusDescription", + "Type": "System.String" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Abort", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "websocketContext", + "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>" + }, + { + "Name": "subProtocol", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.WebSocketAcceptAdapter", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AdaptWebSockets", + "Parameters": [ + { + "Name": "next", + "Type": "System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Func<System.Collections.Generic.IDictionary<System.String, System.Object>, System.Threading.Tasks.Task>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "env", + "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>" + }, + { + "Name": "accept", + "Type": "System.Func<Microsoft.AspNetCore.Http.WebSocketAcceptContext, System.Threading.Tasks.Task<System.Net.WebSockets.WebSocket>>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.WebSocketAdapter", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinEnvironment+FeatureMap", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_CanSet", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "featureInterface", + "Type": "System.Type" + }, + { + "Name": "getter", + "Type": "System.Func<System.Object, System.Object>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "featureInterface", + "Type": "System.Type" + }, + { + "Name": "getter", + "Type": "System.Func<System.Object, System.Object>" + }, + { + "Name": "defaultFactory", + "Type": "System.Func<System.Object>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "featureInterface", + "Type": "System.Type" + }, + { + "Name": "getter", + "Type": "System.Func<System.Object, System.Object>" + }, + { + "Name": "setter", + "Type": "System.Action<System.Object, System.Object>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "featureInterface", + "Type": "System.Type" + }, + { + "Name": "getter", + "Type": "System.Func<System.Object, System.Object>" + }, + { + "Name": "defaultFactory", + "Type": "System.Func<System.Object>" + }, + { + "Name": "setter", + "Type": "System.Action<System.Object, System.Object>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "featureInterface", + "Type": "System.Type" + }, + { + "Name": "getter", + "Type": "System.Func<System.Object, System.Object>" + }, + { + "Name": "defaultFactory", + "Type": "System.Func<System.Object>" + }, + { + "Name": "setter", + "Type": "System.Action<System.Object, System.Object>" + }, + { + "Name": "featureFactory", + "Type": "System.Func<System.Object>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinEnvironment+FeatureMap<T0>", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Owin.OwinEnvironment+FeatureMap", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "getter", + "Type": "System.Func<T0, System.Object>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "getter", + "Type": "System.Func<T0, System.Object>" + }, + { + "Name": "defaultFactory", + "Type": "System.Func<System.Object>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "getter", + "Type": "System.Func<T0, System.Object>" + }, + { + "Name": "setter", + "Type": "System.Action<T0, System.Object>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "getter", + "Type": "System.Func<T0, System.Object>" + }, + { + "Name": "defaultFactory", + "Type": "System.Func<System.Object>" + }, + { + "Name": "setter", + "Type": "System.Action<T0, System.Object>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "getter", + "Type": "System.Func<T0, System.Object>" + }, + { + "Name": "defaultFactory", + "Type": "System.Func<System.Object>" + }, + { + "Name": "setter", + "Type": "System.Action<T0, System.Object>" + }, + { + "Name": "featureFactory", + "Type": "System.Func<T0>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Http/Owin/test/Microsoft.AspNetCore.Owin.Tests.csproj b/src/Http/Owin/test/Microsoft.AspNetCore.Owin.Tests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..359aff75b9914fdfac6d06f38dc5bc0b8a20d8bc --- /dev/null +++ b/src/Http/Owin/test/Microsoft.AspNetCore.Owin.Tests.csproj @@ -0,0 +1,13 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Http" /> + <Reference Include="Microsoft.AspNetCore.Owin" /> + <Reference Include="Microsoft.Extensions.DependencyInjection" /> + </ItemGroup> + +</Project> diff --git a/src/Http/Owin/test/OwinEnvironmentTests.cs b/src/Http/Owin/test/OwinEnvironmentTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..b7288029143608d378e4436629f5d6ec068dce80 --- /dev/null +++ b/src/Http/Owin/test/OwinEnvironmentTests.cs @@ -0,0 +1,148 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Owin +{ + public class OwinEnvironmentTests + { + private T Get<T>(IDictionary<string, object> environment, string key) + { + object value; + return environment.TryGetValue(key, out value) ? (T)value : default(T); + } + + [Fact] + public void OwinEnvironmentCanBeCreated() + { + HttpContext context = CreateContext(); + context.Request.Method = "SomeMethod"; + context.User = new ClaimsPrincipal(new ClaimsIdentity("Foo")); + context.Request.Body = Stream.Null; + context.Request.Headers["CustomRequestHeader"] = "CustomRequestValue"; + context.Request.Path = new PathString("/path"); + context.Request.PathBase = new PathString("/pathBase"); + context.Request.Protocol = "http/1.0"; + context.Request.QueryString = new QueryString("?key=value"); + context.Request.Scheme = "http"; + context.Response.Body = Stream.Null; + context.Response.Headers["CustomResponseHeader"] = "CustomResponseValue"; + context.Response.StatusCode = 201; + + IDictionary<string, object> env = new OwinEnvironment(context); + Assert.Equal("SomeMethod", Get<string>(env, "owin.RequestMethod")); + // User property should set both server.User (non-standard) and owin.RequestUser. + Assert.Equal("Foo", Get<ClaimsPrincipal>(env, "server.User").Identity.AuthenticationType); + Assert.Equal("Foo", Get<ClaimsPrincipal>(env, "owin.RequestUser").Identity.AuthenticationType); + Assert.Same(Stream.Null, Get<Stream>(env, "owin.RequestBody")); + var requestHeaders = Get<IDictionary<string, string[]>>(env, "owin.RequestHeaders"); + Assert.NotNull(requestHeaders); + Assert.Equal("CustomRequestValue", requestHeaders["CustomRequestHeader"].First()); + Assert.Equal("/path", Get<string>(env, "owin.RequestPath")); + Assert.Equal("/pathBase", Get<string>(env, "owin.RequestPathBase")); + Assert.Equal("http/1.0", Get<string>(env, "owin.RequestProtocol")); + Assert.Equal("key=value", Get<string>(env, "owin.RequestQueryString")); + Assert.Equal("http", Get<string>(env, "owin.RequestScheme")); + + Assert.Same(Stream.Null, Get<Stream>(env, "owin.ResponseBody")); + var responseHeaders = Get<IDictionary<string, string[]>>(env, "owin.ResponseHeaders"); + Assert.NotNull(responseHeaders); + Assert.Equal("CustomResponseValue", responseHeaders["CustomResponseHeader"].First()); + Assert.Equal(201, Get<int>(env, "owin.ResponseStatusCode")); + } + + [Fact] + public void OwinEnvironmentCanBeModified() + { + HttpContext context = CreateContext(); + IDictionary<string, object> env = new OwinEnvironment(context); + + env["owin.RequestMethod"] = "SomeMethod"; + env["server.User"] = new ClaimsPrincipal(new ClaimsIdentity("Foo")); + Assert.Equal("Foo", context.User.Identity.AuthenticationType); + // User property should fall back from owin.RequestUser to server.User. + env["owin.RequestUser"] = new ClaimsPrincipal(new ClaimsIdentity("Bar")); + Assert.Equal("Bar", context.User.Identity.AuthenticationType); + env["owin.RequestBody"] = Stream.Null; + var requestHeaders = Get<IDictionary<string, string[]>>(env, "owin.RequestHeaders"); + Assert.NotNull(requestHeaders); + requestHeaders["CustomRequestHeader"] = new[] { "CustomRequestValue" }; + env["owin.RequestPath"] = "/path"; + env["owin.RequestPathBase"] = "/pathBase"; + env["owin.RequestProtocol"] = "http/1.0"; + env["owin.RequestQueryString"] = "key=value"; + env["owin.RequestScheme"] = "http"; + env["owin.ResponseBody"] = Stream.Null; + var responseHeaders = Get<IDictionary<string, string[]>>(env, "owin.ResponseHeaders"); + Assert.NotNull(responseHeaders); + responseHeaders["CustomResponseHeader"] = new[] { "CustomResponseValue" }; + env["owin.ResponseStatusCode"] = 201; + + Assert.Equal("SomeMethod", context.Request.Method); + Assert.Same(Stream.Null, context.Request.Body); + Assert.Equal("CustomRequestValue", context.Request.Headers["CustomRequestHeader"]); + Assert.Equal("/path", context.Request.Path.Value); + Assert.Equal("/pathBase", context.Request.PathBase.Value); + Assert.Equal("http/1.0", context.Request.Protocol); + Assert.Equal("?key=value", context.Request.QueryString.Value); + Assert.Equal("http", context.Request.Scheme); + + Assert.Same(Stream.Null, context.Response.Body); + Assert.Equal("CustomResponseValue", context.Response.Headers["CustomResponseHeader"]); + Assert.Equal(201, context.Response.StatusCode); + } + + [Theory] + [InlineData("server.LocalPort")] + public void OwinEnvironmentDoesNotContainEntriesForMissingFeatures(string key) + { + HttpContext context = CreateContext(); + IDictionary<string, object> env = new OwinEnvironment(context); + + object value; + Assert.False(env.TryGetValue(key, out value)); + + Assert.Throws<KeyNotFoundException>(() => env[key]); + + Assert.False(env.Keys.Contains(key)); + Assert.False(env.ContainsKey(key)); + } + + [Fact] + public void OwinEnvironmentSuppliesDefaultsForMissingRequiredEntries() + { + HttpContext context = CreateContext(); + IDictionary<string, object> env = new OwinEnvironment(context); + + object value; + Assert.True(env.TryGetValue("owin.CallCancelled", out value), "owin.CallCancelled"); + Assert.True(env.TryGetValue("owin.Version", out value), "owin.Version"); + + Assert.Equal(CancellationToken.None, env["owin.CallCancelled"]); + Assert.Equal("1.0", env["owin.Version"]); + } + + [Fact] + public void OwinEnvironmentImpelmentsGetEnumerator() + { + var owinEnvironment = new OwinEnvironment(CreateContext()); + + Assert.NotNull(owinEnvironment.GetEnumerator()); + Assert.NotNull(((IEnumerable)owinEnvironment).GetEnumerator()); + } + + private HttpContext CreateContext() + { + var context = new DefaultHttpContext(); + return context; + } + } +} diff --git a/src/Http/Owin/test/OwinExtensionTests.cs b/src/Http/Owin/test/OwinExtensionTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..c4c51fba0a62fc70e3e0e325c2ed7e6cd148259e --- /dev/null +++ b/src/Http/Owin/test/OwinExtensionTests.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; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Owin +{ + using AddMiddleware = Action<Func< + Func<IDictionary<string, object>, Task>, + Func<IDictionary<string, object>, Task> + >>; + using AppFunc = Func<IDictionary<string, object>, Task>; + using CreateMiddleware = Func< + Func<IDictionary<string, object>, Task>, + Func<IDictionary<string, object>, Task> + >; + + public class OwinExtensionTests + { + static AppFunc notFound = env => new Task(() => { env["owin.ResponseStatusCode"] = 404; }); + + [Fact] + public async Task OwinConfigureServiceProviderAddsServices() + { + var list = new List<CreateMiddleware>(); + AddMiddleware build = list.Add; + IServiceProvider serviceProvider = null; + FakeService fakeService = null; + + var builder = build.UseBuilder(applicationBuilder => + { + serviceProvider = applicationBuilder.ApplicationServices; + applicationBuilder.Run(context => + { + fakeService = context.RequestServices.GetService<FakeService>(); + return Task.FromResult(0); + }); + }, + new ServiceCollection().AddSingleton(new FakeService()).BuildServiceProvider()); + + list.Reverse(); + await list + .Aggregate(notFound, (next, middleware) => middleware(next)) + .Invoke(new Dictionary<string, object>()); + + Assert.NotNull(serviceProvider); + Assert.NotNull(serviceProvider.GetService<FakeService>()); + Assert.NotNull(fakeService); + } + + [Fact] + public async Task OwinDefaultNoServices() + { + var list = new List<CreateMiddleware>(); + AddMiddleware build = list.Add; + IServiceProvider expectedServiceProvider = new ServiceCollection().BuildServiceProvider(); + IServiceProvider serviceProvider = null; + FakeService fakeService = null; + bool builderExecuted = false; + bool applicationExecuted = false; + + var builder = build.UseBuilder(applicationBuilder => + { + builderExecuted = true; + serviceProvider = applicationBuilder.ApplicationServices; + applicationBuilder.Run(context => + { + applicationExecuted = true; + fakeService = context.RequestServices.GetService<FakeService>(); + return Task.FromResult(0); + }); + }, + expectedServiceProvider); + + list.Reverse(); + await list + .Aggregate(notFound, (next, middleware) => middleware(next)) + .Invoke(new Dictionary<string, object>()); + + Assert.True(builderExecuted); + Assert.Equal(expectedServiceProvider, serviceProvider); + Assert.True(applicationExecuted); + Assert.Null(fakeService); + } + + [Fact] + public async Task OwinDefaultNullServiceProvider() + { + var list = new List<CreateMiddleware>(); + AddMiddleware build = list.Add; + IServiceProvider serviceProvider = null; + FakeService fakeService = null; + bool builderExecuted = false; + bool applicationExecuted = false; + + var builder = build.UseBuilder(applicationBuilder => + { + builderExecuted = true; + serviceProvider = applicationBuilder.ApplicationServices; + applicationBuilder.Run(context => + { + applicationExecuted = true; + fakeService = context.RequestServices.GetService<FakeService>(); + return Task.FromResult(0); + }); + }); + + list.Reverse(); + await list + .Aggregate(notFound, (next, middleware) => middleware(next)) + .Invoke(new Dictionary<string, object>()); + + Assert.True(builderExecuted); + Assert.NotNull(serviceProvider); + Assert.True(applicationExecuted); + Assert.Null(fakeService); + } + + [Fact] + public async Task UseOwin() + { + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var builder = new ApplicationBuilder(serviceProvider); + IDictionary<string, object> environment = null; + var context = new DefaultHttpContext(); + + builder.UseOwin(addToPipeline => + { + addToPipeline(next => + { + Assert.NotNull(next); + return async env => + { + environment = env; + await next(env); + }; + }); + }); + await builder.Build().Invoke(context); + + // Dictionary contains context but does not contain "websocket.Accept" or "websocket.AcceptAlt" keys. + Assert.NotNull(environment); + var value = Assert.Single( + environment, + kvp => string.Equals(typeof(HttpContext).FullName, kvp.Key, StringComparison.Ordinal)) + .Value; + Assert.Equal(context, value); + Assert.False(environment.ContainsKey("websocket.Accept")); + Assert.False(environment.ContainsKey("websocket.AcceptAlt")); + } + + private class FakeService + { + } + } +} diff --git a/src/Http/Owin/test/OwinFeatureCollectionTests.cs b/src/Http/Owin/test/OwinFeatureCollectionTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..b2755961c83e37ff5f01c0fd860a1db3befd2fcc --- /dev/null +++ b/src/Http/Owin/test/OwinFeatureCollectionTests.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Owin +{ + public class OwinHttpEnvironmentTests + { + private T Get<T>(IFeatureCollection features) + { + return (T)features[typeof(T)]; + } + + private T Get<T>(IDictionary<string, object> env, string key) + { + object value; + return env.TryGetValue(key, out value) ? (T)value : default(T); + } + + [Fact] + public void OwinHttpEnvironmentCanBeCreated() + { + var env = new Dictionary<string, object> + { + { "owin.RequestMethod", HttpMethods.Post }, + { "owin.RequestPath", "/path" }, + { "owin.RequestPathBase", "/pathBase" }, + { "owin.RequestQueryString", "name=value" }, + }; + var features = new OwinFeatureCollection(env); + + var requestFeature = Get<IHttpRequestFeature>(features); + Assert.Equal(requestFeature.Method, HttpMethods.Post); + Assert.Equal("/path", requestFeature.Path); + Assert.Equal("/pathBase", requestFeature.PathBase); + Assert.Equal("?name=value", requestFeature.QueryString); + } + + [Fact] + public void OwinHttpEnvironmentCanBeModified() + { + var env = new Dictionary<string, object> + { + { "owin.RequestMethod", HttpMethods.Post }, + { "owin.RequestPath", "/path" }, + { "owin.RequestPathBase", "/pathBase" }, + { "owin.RequestQueryString", "name=value" }, + }; + var features = new OwinFeatureCollection(env); + + var requestFeature = Get<IHttpRequestFeature>(features); + requestFeature.Method = HttpMethods.Get; + requestFeature.Path = "/path2"; + requestFeature.PathBase = "/pathBase2"; + requestFeature.QueryString = "?name=value2"; + + Assert.Equal(HttpMethods.Get, Get<string>(env, "owin.RequestMethod")); + Assert.Equal("/path2", Get<string>(env, "owin.RequestPath")); + Assert.Equal("/pathBase2", Get<string>(env, "owin.RequestPathBase")); + Assert.Equal("name=value2", Get<string>(env, "owin.RequestQueryString")); + } + } +} + diff --git a/src/Http/README.md b/src/Http/README.md new file mode 100644 index 0000000000000000000000000000000000000000..58e2500a02c84db7c7c5f43645e86b09dab17a2e --- /dev/null +++ b/src/Http/README.md @@ -0,0 +1,6 @@ +Http Abstractions +================= + +This folders contains projects for HTTP abstractions for ASP.NET Core such as `HttpContext`, `HttpRequest`, `HttpResponse` and `RequestDelegate`. + +It also contains `IApplicationBuilder` and extensions to create and compose your application's pipeline. diff --git a/src/Http/WebUtilities/src/Base64UrlTextEncoder.cs b/src/Http/WebUtilities/src/Base64UrlTextEncoder.cs new file mode 100644 index 0000000000000000000000000000000000000000..304ee6522f813dc66372a14f580c4b8ca705a709 --- /dev/null +++ b/src/Http/WebUtilities/src/Base64UrlTextEncoder.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.WebUtilities +{ + public static class Base64UrlTextEncoder + { + /// <summary> + /// Encodes supplied data into Base64 and replaces any URL encodable characters into non-URL encodable + /// characters. + /// </summary> + /// <param name="data">Data to be encoded.</param> + /// <returns>Base64 encoded string modified with non-URL encodable characters</returns> + public static string Encode(byte[] data) + { + return WebEncoders.Base64UrlEncode(data); + } + + /// <summary> + /// Decodes supplied string by replacing the non-URL encodable characters with URL encodable characters and + /// then decodes the Base64 string. + /// </summary> + /// <param name="text">The string to be decoded.</param> + /// <returns>The decoded data.</returns> + public static byte[] Decode(string text) + { + return WebEncoders.Base64UrlDecode(text); + } + } +} diff --git a/src/Http/WebUtilities/src/BufferedReadStream.cs b/src/Http/WebUtilities/src/BufferedReadStream.cs new file mode 100644 index 0000000000000000000000000000000000000000..10f1465f3a2e2b71d9ef0631350af765d7094989 --- /dev/null +++ b/src/Http/WebUtilities/src/BufferedReadStream.cs @@ -0,0 +1,431 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// <summary> + /// A Stream that wraps another stream and allows reading lines. + /// The data is buffered in memory. + /// </summary> + public class BufferedReadStream : Stream + { + private const byte CR = (byte)'\r'; + private const byte LF = (byte)'\n'; + + private readonly Stream _inner; + private readonly byte[] _buffer; + private readonly ArrayPool<byte> _bytePool; + private int _bufferOffset = 0; + private int _bufferCount = 0; + private bool _disposed; + + /// <summary> + /// Creates a new stream. + /// </summary> + /// <param name="inner">The stream to wrap.</param> + /// <param name="bufferSize">Size of buffer in bytes.</param> + public BufferedReadStream(Stream inner, int bufferSize) + : this(inner, bufferSize, ArrayPool<byte>.Shared) + { + } + + /// <summary> + /// Creates a new stream. + /// </summary> + /// <param name="inner">The stream to wrap.</param> + /// <param name="bufferSize">Size of buffer in bytes.</param> + /// <param name="bytePool">ArrayPool for the buffer.</param> + public BufferedReadStream(Stream inner, int bufferSize, ArrayPool<byte> bytePool) + { + if (inner == null) + { + throw new ArgumentNullException(nameof(inner)); + } + + _inner = inner; + _bytePool = bytePool; + _buffer = bytePool.Rent(bufferSize); + } + + /// <summary> + /// The currently buffered data. + /// </summary> + public ArraySegment<byte> BufferedData + { + get { return new ArraySegment<byte>(_buffer, _bufferOffset, _bufferCount); } + } + + /// <inheritdoc/> + public override bool CanRead + { + get { return _inner.CanRead || _bufferCount > 0; } + } + + /// <inheritdoc/> + public override bool CanSeek + { + get { return _inner.CanSeek; } + } + + /// <inheritdoc/> + public override bool CanTimeout + { + get { return _inner.CanTimeout; } + } + + /// <inheritdoc/> + public override bool CanWrite + { + get { return _inner.CanWrite; } + } + + /// <inheritdoc/> + public override long Length + { + get { return _inner.Length; } + } + + /// <inheritdoc/> + public override long Position + { + get { return _inner.Position - _bufferCount; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "Position must be positive."); + } + if (value == Position) + { + return; + } + + // Backwards? + if (value <= _inner.Position) + { + // Forward within the buffer? + var innerOffset = (int)(_inner.Position - value); + if (innerOffset <= _bufferCount) + { + // Yes, just skip some of the buffered data + _bufferOffset += innerOffset; + _bufferCount -= innerOffset; + } + else + { + // No, reset the buffer + _bufferOffset = 0; + _bufferCount = 0; + _inner.Position = value; + } + } + else + { + // Forward, reset the buffer + _bufferOffset = 0; + _bufferCount = 0; + _inner.Position = value; + } + } + } + + /// <inheritdoc/> + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + Position = offset; + } + else if (origin == SeekOrigin.Current) + { + Position = Position + offset; + } + else // if (origin == SeekOrigin.End) + { + Position = Length + offset; + } + return Position; + } + + /// <inheritdoc/> + public override void SetLength(long value) + { + _inner.SetLength(value); + } + + /// <inheritdoc/> + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + _bytePool.Return(_buffer); + + if (disposing) + { + _inner.Dispose(); + } + } + } + + /// <inheritdoc/> + public override void Flush() + { + _inner.Flush(); + } + + /// <inheritdoc/> + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } + + /// <inheritdoc/> + public override void Write(byte[] buffer, int offset, int count) + { + _inner.Write(buffer, offset, count); + } + + /// <inheritdoc/> + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + /// <inheritdoc/> + public override int Read(byte[] buffer, int offset, int count) + { + ValidateBuffer(buffer, offset, count); + + // Drain buffer + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, count); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + return toCopy; + } + + return _inner.Read(buffer, offset, count); + } + + /// <inheritdoc/> + public async override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBuffer(buffer, offset, count); + + // Drain buffer + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, count); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + return toCopy; + } + + return await _inner.ReadAsync(buffer, offset, count, cancellationToken); + } + + /// <summary> + /// Ensures that the buffer is not empty. + /// </summary> + /// <returns>Returns <c>true</c> if the buffer is not empty; <c>false</c> otherwise.</returns> + public bool EnsureBuffered() + { + if (_bufferCount > 0) + { + return true; + } + // Downshift to make room + _bufferOffset = 0; + _bufferCount = _inner.Read(_buffer, 0, _buffer.Length); + return _bufferCount > 0; + } + + /// <summary> + /// Ensures that the buffer is not empty. + /// </summary> + /// <param name="cancellationToken">Cancellation token.</param> + /// <returns>Returns <c>true</c> if the buffer is not empty; <c>false</c> otherwise.</returns> + public async Task<bool> EnsureBufferedAsync(CancellationToken cancellationToken) + { + if (_bufferCount > 0) + { + return true; + } + // Downshift to make room + _bufferOffset = 0; + _bufferCount = await _inner.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken); + return _bufferCount > 0; + } + + /// <summary> + /// Ensures that a minimum amount of buffered data is available. + /// </summary> + /// <param name="minCount">Minimum amount of buffered data.</param> + /// <returns>Returns <c>true</c> if the minimum amount of buffered data is available; <c>false</c> otherwise.</returns> + public bool EnsureBuffered(int minCount) + { + if (minCount > _buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length.ToString()); + } + while (_bufferCount < minCount) + { + // Downshift to make room + if (_bufferOffset > 0) + { + if (_bufferCount > 0) + { + Buffer.BlockCopy(_buffer, _bufferOffset, _buffer, 0, _bufferCount); + } + _bufferOffset = 0; + } + int read = _inner.Read(_buffer, _bufferOffset + _bufferCount, _buffer.Length - _bufferCount - _bufferOffset); + _bufferCount += read; + if (read == 0) + { + return false; + } + } + return true; + } + + /// <summary> + /// Ensures that a minimum amount of buffered data is available. + /// </summary> + /// <param name="minCount">Minimum amount of buffered data.</param> + /// <param name="cancellationToken">Cancellation token.</param> + /// <returns>Returns <c>true</c> if the minimum amount of buffered data is available; <c>false</c> otherwise.</returns> + public async Task<bool> EnsureBufferedAsync(int minCount, CancellationToken cancellationToken) + { + if (minCount > _buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length.ToString()); + } + while (_bufferCount < minCount) + { + // Downshift to make room + if (_bufferOffset > 0) + { + if (_bufferCount > 0) + { + Buffer.BlockCopy(_buffer, _bufferOffset, _buffer, 0, _bufferCount); + } + _bufferOffset = 0; + } + int read = await _inner.ReadAsync(_buffer, _bufferOffset + _bufferCount, _buffer.Length - _bufferCount - _bufferOffset, cancellationToken); + _bufferCount += read; + if (read == 0) + { + return false; + } + } + return true; + } + + /// <summary> + /// Reads a line. A line is defined as a sequence of characters followed by + /// a carriage return immediately followed by a line feed. The resulting string does not + /// contain the terminating carriage return and line feed. + /// </summary> + /// <param name="lengthLimit">Maximum allowed line length.</param> + /// <returns>A line.</returns> + public string ReadLine(int lengthLimit) + { + CheckDisposed(); + using (var builder = new MemoryStream(200)) + { + bool foundCR = false, foundCRLF = false; + + while (!foundCRLF && EnsureBuffered()) + { + if (builder.Length > lengthLimit) + { + throw new InvalidDataException($"Line length limit {lengthLimit} exceeded."); + } + ProcessLineChar(builder, ref foundCR, ref foundCRLF); + } + + return DecodeLine(builder, foundCRLF); + } + } + + /// <summary> + /// Reads a line. A line is defined as a sequence of characters followed by + /// a carriage return immediately followed by a line feed. The resulting string does not + /// contain the terminating carriage return and line feed. + /// </summary> + /// <param name="lengthLimit">Maximum allowed line length.</param> + /// <param name="cancellationToken">Cancellation token.</param> + /// <returns>A line.</returns> + public async Task<string> ReadLineAsync(int lengthLimit, CancellationToken cancellationToken) + { + CheckDisposed(); + using (var builder = new MemoryStream(200)) + { + bool foundCR = false, foundCRLF = false; + + while (!foundCRLF && await EnsureBufferedAsync(cancellationToken)) + { + if (builder.Length > lengthLimit) + { + throw new InvalidDataException($"Line length limit {lengthLimit} exceeded."); + } + + ProcessLineChar(builder, ref foundCR, ref foundCRLF); + } + + return DecodeLine(builder, foundCRLF); + } + } + + private void ProcessLineChar(MemoryStream builder, ref bool foundCR, ref bool foundCRLF) + { + var b = _buffer[_bufferOffset]; + builder.WriteByte(b); + _bufferOffset++; + _bufferCount--; + if (b == LF && foundCR) + { + foundCRLF = true; + return; + } + foundCR = b == CR; + } + + private string DecodeLine(MemoryStream builder, bool foundCRLF) + { + // Drop the final CRLF, if any + var length = foundCRLF ? builder.Length - 2 : builder.Length; + return Encoding.UTF8.GetString(builder.ToArray(), 0, (int)length); + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(BufferedReadStream)); + } + } + + private void ValidateBuffer(byte[] buffer, int offset, int count) + { + // Delegate most of our validation. + var ignored = new ArraySegment<byte>(buffer, offset, count); + if (count == 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "The value must be greater than zero."); + } + } + } +} diff --git a/src/Http/WebUtilities/src/FileBufferingReadStream.cs b/src/Http/WebUtilities/src/FileBufferingReadStream.cs new file mode 100644 index 0000000000000000000000000000000000000000..9dd1fbf13f3c3821dc612d32a4628952b4e8479c --- /dev/null +++ b/src/Http/WebUtilities/src/FileBufferingReadStream.cs @@ -0,0 +1,354 @@ +// 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.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// <summary> + /// A Stream that wraps another stream and enables rewinding by buffering the content as it is read. + /// The content is buffered in memory up to a certain size and then spooled to a temp file on disk. + /// The temp file will be deleted on Dispose. + /// </summary> + public class FileBufferingReadStream : Stream + { + private const int _maxRentedBufferSize = 1024 * 1024; // 1MB + private readonly Stream _inner; + private readonly ArrayPool<byte> _bytePool; + private readonly int _memoryThreshold; + private readonly long? _bufferLimit; + private string _tempFileDirectory; + private readonly Func<string> _tempFileDirectoryAccessor; + private string _tempFileName; + + private Stream _buffer; + private byte[] _rentedBuffer; + private bool _inMemory = true; + private bool _completelyBuffered; + + private bool _disposed; + + public FileBufferingReadStream( + Stream inner, + int memoryThreshold, + long? bufferLimit, + Func<string> tempFileDirectoryAccessor) + : this(inner, memoryThreshold, bufferLimit, tempFileDirectoryAccessor, ArrayPool<byte>.Shared) + { + } + + public FileBufferingReadStream( + Stream inner, + int memoryThreshold, + long? bufferLimit, + Func<string> tempFileDirectoryAccessor, + ArrayPool<byte> bytePool) + { + if (inner == null) + { + throw new ArgumentNullException(nameof(inner)); + } + + if (tempFileDirectoryAccessor == null) + { + throw new ArgumentNullException(nameof(tempFileDirectoryAccessor)); + } + + _bytePool = bytePool; + if (memoryThreshold < _maxRentedBufferSize) + { + _rentedBuffer = bytePool.Rent(memoryThreshold); + _buffer = new MemoryStream(_rentedBuffer); + _buffer.SetLength(0); + } + else + { + _buffer = new MemoryStream(); + } + + _inner = inner; + _memoryThreshold = memoryThreshold; + _bufferLimit = bufferLimit; + _tempFileDirectoryAccessor = tempFileDirectoryAccessor; + } + + public FileBufferingReadStream( + Stream inner, + int memoryThreshold, + long? bufferLimit, + string tempFileDirectory) + : this(inner, memoryThreshold, bufferLimit, tempFileDirectory, ArrayPool<byte>.Shared) + { + } + + public FileBufferingReadStream( + Stream inner, + int memoryThreshold, + long? bufferLimit, + string tempFileDirectory, + ArrayPool<byte> bytePool) + { + if (inner == null) + { + throw new ArgumentNullException(nameof(inner)); + } + + if (tempFileDirectory == null) + { + throw new ArgumentNullException(nameof(tempFileDirectory)); + } + + _bytePool = bytePool; + if (memoryThreshold < _maxRentedBufferSize) + { + _rentedBuffer = bytePool.Rent(memoryThreshold); + _buffer = new MemoryStream(_rentedBuffer); + _buffer.SetLength(0); + } + else + { + _buffer = new MemoryStream(); + } + + _inner = inner; + _memoryThreshold = memoryThreshold; + _bufferLimit = bufferLimit; + _tempFileDirectory = tempFileDirectory; + } + + public bool InMemory + { + get { return _inMemory; } + } + + public string TempFileName + { + get { return _tempFileName; } + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _buffer.Length; } + } + + public override long Position + { + get { return _buffer.Position; } + // Note this will not allow seeking forward beyond the end of the buffer. + set + { + ThrowIfDisposed(); + _buffer.Position = value; + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + if (!_completelyBuffered && origin == SeekOrigin.End) + { + // Can't seek from the end until we've finished consuming the inner stream + throw new NotSupportedException("The content has not been fully buffered yet."); + } + else if (!_completelyBuffered && origin == SeekOrigin.Current && offset + Position > Length) + { + // Can't seek past the end of the buffer until we've finished consuming the inner stream + throw new NotSupportedException("The content has not been fully buffered yet."); + } + else if (!_completelyBuffered && origin == SeekOrigin.Begin && offset > Length) + { + // Can't seek past the end of the buffer until we've finished consuming the inner stream + throw new NotSupportedException("The content has not been fully buffered yet."); + } + return _buffer.Seek(offset, origin); + } + + private Stream CreateTempFile() + { + if (_tempFileDirectory == null) + { + Debug.Assert(_tempFileDirectoryAccessor != null); + _tempFileDirectory = _tempFileDirectoryAccessor(); + Debug.Assert(_tempFileDirectory != null); + } + + _tempFileName = Path.Combine(_tempFileDirectory, "ASPNETCORE_" + Guid.NewGuid().ToString() + ".tmp"); + return new FileStream(_tempFileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Delete, 1024 * 16, + FileOptions.Asynchronous | FileOptions.DeleteOnClose | FileOptions.SequentialScan); + } + + public override int Read(byte[] buffer, int offset, int count) + { + ThrowIfDisposed(); + if (_buffer.Position < _buffer.Length || _completelyBuffered) + { + // Just read from the buffer + return _buffer.Read(buffer, offset, (int)Math.Min(count, _buffer.Length - _buffer.Position)); + } + + int read = _inner.Read(buffer, offset, count); + + if (_bufferLimit.HasValue && _bufferLimit - read < _buffer.Length) + { + Dispose(); + throw new IOException("Buffer limit exceeded."); + } + + if (_inMemory && _buffer.Length + read > _memoryThreshold) + { + _inMemory = false; + var oldBuffer = _buffer; + _buffer = CreateTempFile(); + if (_rentedBuffer == null) + { + oldBuffer.Position = 0; + var rentedBuffer = _bytePool.Rent(Math.Min((int)oldBuffer.Length, _maxRentedBufferSize)); + var copyRead = oldBuffer.Read(rentedBuffer, 0, rentedBuffer.Length); + while (copyRead > 0) + { + _buffer.Write(rentedBuffer, 0, copyRead); + copyRead = oldBuffer.Read(rentedBuffer, 0, rentedBuffer.Length); + } + _bytePool.Return(rentedBuffer); + } + else + { + _buffer.Write(_rentedBuffer, 0, (int)oldBuffer.Length); + _bytePool.Return(_rentedBuffer); + _rentedBuffer = null; + } + } + + if (read > 0) + { + _buffer.Write(buffer, offset, read); + } + else + { + _completelyBuffered = true; + } + + return read; + } + + public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + if (_buffer.Position < _buffer.Length || _completelyBuffered) + { + // Just read from the buffer + return await _buffer.ReadAsync(buffer, offset, (int)Math.Min(count, _buffer.Length - _buffer.Position), cancellationToken); + } + + int read = await _inner.ReadAsync(buffer, offset, count, cancellationToken); + + if (_bufferLimit.HasValue && _bufferLimit - read < _buffer.Length) + { + Dispose(); + throw new IOException("Buffer limit exceeded."); + } + + if (_inMemory && _buffer.Length + read > _memoryThreshold) + { + _inMemory = false; + var oldBuffer = _buffer; + _buffer = CreateTempFile(); + if (_rentedBuffer == null) + { + oldBuffer.Position = 0; + var rentedBuffer = _bytePool.Rent(Math.Min((int)oldBuffer.Length, _maxRentedBufferSize)); + // oldBuffer is a MemoryStream, no need to do async reads. + var copyRead = oldBuffer.Read(rentedBuffer, 0, rentedBuffer.Length); + while (copyRead > 0) + { + await _buffer.WriteAsync(rentedBuffer, 0, copyRead, cancellationToken); + copyRead = oldBuffer.Read(rentedBuffer, 0, rentedBuffer.Length); + } + _bytePool.Return(rentedBuffer); + } + else + { + await _buffer.WriteAsync(_rentedBuffer, 0, (int)oldBuffer.Length, cancellationToken); + _bytePool.Return(_rentedBuffer); + _rentedBuffer = null; + } + } + + if (read > 0) + { + await _buffer.WriteAsync(buffer, offset, read, cancellationToken); + } + else + { + _completelyBuffered = true; + } + + return read; + } + + 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 SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + if (_rentedBuffer != null) + { + _bytePool.Return(_rentedBuffer); + } + + if (disposing) + { + _buffer.Dispose(); + } + } + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(FileBufferingReadStream)); + } + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/FileMultipartSection.cs b/src/Http/WebUtilities/src/FileMultipartSection.cs new file mode 100644 index 0000000000000000000000000000000000000000..70d7741f64d5624d8015112810a7fe5738dcc643 --- /dev/null +++ b/src/Http/WebUtilities/src/FileMultipartSection.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// <summary> + /// Represents a file multipart section + /// </summary> + public class FileMultipartSection + { + private ContentDispositionHeaderValue _contentDispositionHeader; + + /// <summary> + /// Creates a new instance of the <see cref="FileMultipartSection"/> class + /// </summary> + /// <param name="section">The section from which to create the <see cref="FileMultipartSection"/></param> + /// <remarks>Reparses the content disposition header</remarks> + public FileMultipartSection(MultipartSection section) + :this(section, section.GetContentDispositionHeader()) + { + } + + /// <summary> + /// Creates a new instance of the <see cref="FileMultipartSection"/> class + /// </summary> + /// <param name="section">The section from which to create the <see cref="FileMultipartSection"/></param> + /// <param name="header">An already parsed content disposition header</param> + public FileMultipartSection(MultipartSection section, ContentDispositionHeaderValue header) + { + if (!header.IsFileDisposition()) + { + throw new ArgumentException($"Argument must be a file section", nameof(section)); + } + + Section = section; + _contentDispositionHeader = header; + + Name = HeaderUtilities.RemoveQuotes(_contentDispositionHeader.Name).ToString(); + FileName = HeaderUtilities.RemoveQuotes( + _contentDispositionHeader.FileNameStar.HasValue ? + _contentDispositionHeader.FileNameStar : + _contentDispositionHeader.FileName).ToString(); + } + + /// <summary> + /// Gets the original section from which this object was created + /// </summary> + public MultipartSection Section { get; } + + /// <summary> + /// Gets the file stream from the section body + /// </summary> + public Stream FileStream => Section.Body; + + /// <summary> + /// Gets the name of the section + /// </summary> + public string Name { get; } + + /// <summary> + /// Gets the name of the file from the section + /// </summary> + public string FileName { get; } + + } +} diff --git a/src/Http/WebUtilities/src/FormMultipartSection.cs b/src/Http/WebUtilities/src/FormMultipartSection.cs new file mode 100644 index 0000000000000000000000000000000000000000..01af0455b8809283875846b288aa2b4cecc1fd69 --- /dev/null +++ b/src/Http/WebUtilities/src/FormMultipartSection.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// <summary> + /// Represents a form multipart section + /// </summary> + public class FormMultipartSection + { + private ContentDispositionHeaderValue _contentDispositionHeader; + + /// <summary> + /// Creates a new instance of the <see cref="FormMultipartSection"/> class + /// </summary> + /// <param name="section">The section from which to create the <see cref="FormMultipartSection"/></param> + /// <remarks>Reparses the content disposition header</remarks> + public FormMultipartSection(MultipartSection section) + : this(section, section.GetContentDispositionHeader()) + { + } + + /// <summary> + /// Creates a new instance of the <see cref="FormMultipartSection"/> class + /// </summary> + /// <param name="section">The section from which to create the <see cref="FormMultipartSection"/></param> + /// <param name="header">An already parsed content disposition header</param> + public FormMultipartSection(MultipartSection section, ContentDispositionHeaderValue header) + { + if (header == null || !header.IsFormDisposition()) + { + throw new ArgumentException($"Argument must be a form section", nameof(section)); + } + + Section = section; + _contentDispositionHeader = header; + Name = HeaderUtilities.RemoveQuotes(_contentDispositionHeader.Name).ToString(); + } + + /// <summary> + /// Gets the original section from which this object was created + /// </summary> + public MultipartSection Section { get; } + + /// <summary> + /// The form name + /// </summary> + public string Name { get; } + + /// <summary> + /// Gets the form value + /// </summary> + /// <returns>The form value</returns> + public Task<string> GetValueAsync() + { + return Section.ReadAsStringAsync(); + } + } +} diff --git a/src/Http/WebUtilities/src/FormReader.cs b/src/Http/WebUtilities/src/FormReader.cs new file mode 100644 index 0000000000000000000000000000000000000000..958a4971fab7e4651379129fc48fab6d135d1b8f --- /dev/null +++ b/src/Http/WebUtilities/src/FormReader.cs @@ -0,0 +1,312 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// <summary> + /// Used to read an 'application/x-www-form-urlencoded' form. + /// </summary> + public class FormReader : IDisposable + { + public const int DefaultValueCountLimit = 1024; + public const int DefaultKeyLengthLimit = 1024 * 2; + public const int DefaultValueLengthLimit = 1024 * 1024 * 4; + + private const int _rentedCharPoolLength = 8192; + private readonly TextReader _reader; + private readonly char[] _buffer; + private readonly ArrayPool<char> _charPool; + private readonly StringBuilder _builder = new StringBuilder(); + private int _bufferOffset; + private int _bufferCount; + private string _currentKey; + private string _currentValue; + private bool _endOfStream; + private bool _disposed; + + public FormReader(string data) + : this(data, ArrayPool<char>.Shared) + { + } + + public FormReader(string data, ArrayPool<char> charPool) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + _buffer = charPool.Rent(_rentedCharPoolLength); + _charPool = charPool; + _reader = new StringReader(data); + } + + public FormReader(Stream stream) + : this(stream, Encoding.UTF8, ArrayPool<char>.Shared) + { + } + + public FormReader(Stream stream, Encoding encoding) + : this(stream, encoding, ArrayPool<char>.Shared) + { + } + + public FormReader(Stream stream, Encoding encoding, ArrayPool<char> charPool) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + + _buffer = charPool.Rent(_rentedCharPoolLength); + _charPool = charPool; + _reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true); + } + + /// <summary> + /// The limit on the number of form values to allow in ReadForm or ReadFormAsync. + /// </summary> + public int ValueCountLimit { get; set; } = DefaultValueCountLimit; + + /// <summary> + /// The limit on the length of form keys. + /// </summary> + public int KeyLengthLimit { get; set; } = DefaultKeyLengthLimit; + + /// <summary> + /// The limit on the length of form values. + /// </summary> + public int ValueLengthLimit { get; set; } = DefaultValueLengthLimit; + + // Format: key1=value1&key2=value2 + /// <summary> + /// Reads the next key value pair from the form. + /// For unbuffered data use the async overload instead. + /// </summary> + /// <returns>The next key value pair, or null when the end of the form is reached.</returns> + public KeyValuePair<string, string>? ReadNextPair() + { + ReadNextPairImpl(); + if (ReadSucceded()) + { + return new KeyValuePair<string, string>(_currentKey, _currentValue); + } + return null; + } + + private void ReadNextPairImpl() + { + StartReadNextPair(); + while (!_endOfStream) + { + // Empty + if (_bufferCount == 0) + { + Buffer(); + } + if (TryReadNextPair()) + { + break; + } + } + } + + // Format: key1=value1&key2=value2 + /// <summary> + /// Asynchronously reads the next key value pair from the form. + /// </summary> + /// <param name="cancellationToken"></param> + /// <returns>The next key value pair, or null when the end of the form is reached.</returns> + public async Task<KeyValuePair<string, string>?> ReadNextPairAsync(CancellationToken cancellationToken = new CancellationToken()) + { + await ReadNextPairAsyncImpl(cancellationToken); + if (ReadSucceded()) + { + return new KeyValuePair<string, string>(_currentKey, _currentValue); + } + return null; + } + + private async Task ReadNextPairAsyncImpl(CancellationToken cancellationToken = new CancellationToken()) + { + StartReadNextPair(); + while (!_endOfStream) + { + // Empty + if (_bufferCount == 0) + { + await BufferAsync(cancellationToken); + } + if (TryReadNextPair()) + { + break; + } + } + } + + private void StartReadNextPair() + { + _currentKey = null; + _currentValue = null; + } + + private bool TryReadNextPair() + { + if (_currentKey == null) + { + if (!TryReadWord('=', KeyLengthLimit, out _currentKey)) + { + return false; + } + + if (_bufferCount == 0) + { + return false; + } + } + + if (_currentValue == null) + { + if (!TryReadWord('&', ValueLengthLimit, out _currentValue)) + { + return false; + } + } + return true; + } + + private bool TryReadWord(char seperator, int limit, out string value) + { + do + { + if (ReadChar(seperator, limit, out value)) + { + return true; + } + } while (_bufferCount > 0); + return false; + } + + private bool ReadChar(char seperator, int limit, out string word) + { + // End + if (_bufferCount == 0) + { + word = BuildWord(); + return true; + } + + var c = _buffer[_bufferOffset++]; + _bufferCount--; + + if (c == seperator) + { + word = BuildWord(); + return true; + } + if (_builder.Length >= limit) + { + throw new InvalidDataException($"Form key or value length limit {limit} exceeded."); + } + _builder.Append(c); + word = null; + return false; + } + + // '+' un-escapes to ' ', %HH un-escapes as ASCII (or utf-8?) + private string BuildWord() + { + _builder.Replace('+', ' '); + var result = _builder.ToString(); + _builder.Clear(); + return Uri.UnescapeDataString(result); // TODO: Replace this, it's not completely accurate. + } + + private void Buffer() + { + _bufferOffset = 0; + _bufferCount = _reader.Read(_buffer, 0, _buffer.Length); + _endOfStream = _bufferCount == 0; + } + + private async Task BufferAsync(CancellationToken cancellationToken) + { + // TODO: StreamReader doesn't support cancellation? + cancellationToken.ThrowIfCancellationRequested(); + _bufferOffset = 0; + _bufferCount = await _reader.ReadAsync(_buffer, 0, _buffer.Length); + _endOfStream = _bufferCount == 0; + } + + /// <summary> + /// Parses text from an HTTP form body. + /// </summary> + /// <returns>The collection containing the parsed HTTP form body.</returns> + public Dictionary<string, StringValues> ReadForm() + { + var accumulator = new KeyValueAccumulator(); + while (!_endOfStream) + { + ReadNextPairImpl(); + Append(ref accumulator); + } + return accumulator.GetResults(); + } + + /// <summary> + /// Parses an HTTP form body. + /// </summary> + /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> + /// <returns>The collection containing the parsed HTTP form body.</returns> + public async Task<Dictionary<string, StringValues>> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken()) + { + var accumulator = new KeyValueAccumulator(); + while (!_endOfStream) + { + await ReadNextPairAsyncImpl(cancellationToken); + Append(ref accumulator); + } + return accumulator.GetResults(); + } + + private bool ReadSucceded() + { + return _currentKey != null && _currentValue != null; + } + + private void Append(ref KeyValueAccumulator accumulator) + { + if (ReadSucceded()) + { + accumulator.Append(_currentKey, _currentValue); + if (accumulator.ValueCount > ValueCountLimit) + { + throw new InvalidDataException($"Form value count limit {ValueCountLimit} exceeded."); + } + } + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + _charPool.Return(_buffer); + } + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/HttpRequestStreamReader.cs b/src/Http/WebUtilities/src/HttpRequestStreamReader.cs new file mode 100644 index 0000000000000000000000000000000000000000..3f9478c5deaab88a443c7b3376b17cc4ebee9671 --- /dev/null +++ b/src/Http/WebUtilities/src/HttpRequestStreamReader.cs @@ -0,0 +1,374 @@ +// 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.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class HttpRequestStreamReader : TextReader + { + private const int DefaultBufferSize = 1024; + private const int MinBufferSize = 128; + private const int MaxSharedBuilderCapacity = 360; // also the max capacity used in StringBuilderCache + + private Stream _stream; + private readonly Encoding _encoding; + private readonly Decoder _decoder; + + private readonly ArrayPool<byte> _bytePool; + private readonly ArrayPool<char> _charPool; + + private readonly int _byteBufferSize; + private byte[] _byteBuffer; + private char[] _charBuffer; + + private int _charBufferIndex; + private int _charsRead; + private int _bytesRead; + + private bool _isBlocked; + private bool _disposed; + + public HttpRequestStreamReader(Stream stream, Encoding encoding) + : this(stream, encoding, DefaultBufferSize, ArrayPool<byte>.Shared, ArrayPool<char>.Shared) + { + } + + public HttpRequestStreamReader(Stream stream, Encoding encoding, int bufferSize) + : this(stream, encoding, bufferSize, ArrayPool<byte>.Shared, ArrayPool<char>.Shared) + { + } + + public HttpRequestStreamReader( + Stream stream, + Encoding encoding, + int bufferSize, + ArrayPool<byte> bytePool, + ArrayPool<char> charPool) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + _encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); + _bytePool = bytePool ?? throw new ArgumentNullException(nameof(bytePool)); + _charPool = charPool ?? throw new ArgumentNullException(nameof(charPool)); + + if (bufferSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + if (!stream.CanRead) + { + throw new ArgumentException(Resources.HttpRequestStreamReader_StreamNotReadable, nameof(stream)); + } + + _byteBufferSize = bufferSize; + + _decoder = encoding.GetDecoder(); + _byteBuffer = _bytePool.Rent(bufferSize); + + try + { + var requiredLength = encoding.GetMaxCharCount(bufferSize); + _charBuffer = _charPool.Rent(requiredLength); + } + catch + { + _bytePool.Return(_byteBuffer); + + if (_charBuffer != null) + { + _charPool.Return(_charBuffer); + } + + throw; + } + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + + _bytePool.Return(_byteBuffer); + _charPool.Return(_charBuffer); + } + + base.Dispose(disposing); + } + + public override int Peek() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); + } + + if (_charBufferIndex == _charsRead) + { + if (_isBlocked || ReadIntoBuffer() == 0) + { + return -1; + } + } + + return _charBuffer[_charBufferIndex]; + } + + public override int Read() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); + } + + if (_charBufferIndex == _charsRead) + { + if (ReadIntoBuffer() == 0) + { + return -1; + } + } + + return _charBuffer[_charBufferIndex++]; + } + + public override int Read(char[] buffer, int index, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (count < 0 || index + count > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); + } + + var charsRead = 0; + while (count > 0) + { + var charsRemaining = _charsRead - _charBufferIndex; + if (charsRemaining == 0) + { + charsRemaining = ReadIntoBuffer(); + } + + if (charsRemaining == 0) + { + break; // We're at EOF + } + + if (charsRemaining > count) + { + charsRemaining = count; + } + + Buffer.BlockCopy( + _charBuffer, + _charBufferIndex * 2, + buffer, + (index + charsRead) * 2, + charsRemaining * 2); + _charBufferIndex += charsRemaining; + + charsRead += charsRemaining; + count -= charsRemaining; + + // If we got back fewer chars than we asked for, then it's likely the underlying stream is blocked. + // Send the data back to the caller so they can process it. + if (_isBlocked) + { + break; + } + } + + return charsRead; + } + + public override async Task<int> ReadAsync(char[] buffer, int index, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (count < 0 || index + count > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); + } + + if (_charBufferIndex == _charsRead && await ReadIntoBufferAsync() == 0) + { + return 0; + } + + var charsRead = 0; + while (count > 0) + { + // n is the characters available in _charBuffer + var n = _charsRead - _charBufferIndex; + + // charBuffer is empty, let's read from the stream + if (n == 0) + { + _charsRead = 0; + _charBufferIndex = 0; + _bytesRead = 0; + + // We loop here so that we read in enough bytes to yield at least 1 char. + // We break out of the loop if the stream is blocked (EOF is reached). + do + { + Debug.Assert(n == 0); + _bytesRead = await _stream.ReadAsync( + _byteBuffer, + 0, + _byteBufferSize); + if (_bytesRead == 0) // EOF + { + _isBlocked = true; + break; + } + + // _isBlocked == whether we read fewer bytes than we asked for. + _isBlocked = (_bytesRead < _byteBufferSize); + + Debug.Assert(n == 0); + + _charBufferIndex = 0; + n = _decoder.GetChars( + _byteBuffer, + 0, + _bytesRead, + _charBuffer, + 0); + + Debug.Assert(n > 0); + + _charsRead += n; // Number of chars in StreamReader's buffer. + } + while (n == 0); + + if (n == 0) + { + break; // We're at EOF + } + } + + // Got more chars in charBuffer than the user requested + if (n > count) + { + n = count; + } + + Buffer.BlockCopy( + _charBuffer, + _charBufferIndex * 2, + buffer, + (index + charsRead) * 2, + n * 2); + + _charBufferIndex += n; + + charsRead += n; + count -= n; + + // This function shouldn't block for an indefinite amount of time, + // or reading from a network stream won't work right. If we got + // fewer bytes than we requested, then we want to break right here. + if (_isBlocked) + { + break; + } + } + + return charsRead; + } + + private int ReadIntoBuffer() + { + _charsRead = 0; + _charBufferIndex = 0; + _bytesRead = 0; + + do + { + _bytesRead = _stream.Read(_byteBuffer, 0, _byteBufferSize); + if (_bytesRead == 0) // We're at EOF + { + return _charsRead; + } + + _isBlocked = (_bytesRead < _byteBufferSize); + _charsRead += _decoder.GetChars( + _byteBuffer, + 0, + _bytesRead, + _charBuffer, + _charsRead); + } + while (_charsRead == 0); + + return _charsRead; + } + + private async Task<int> ReadIntoBufferAsync() + { + _charsRead = 0; + _charBufferIndex = 0; + _bytesRead = 0; + + do + { + + _bytesRead = await _stream.ReadAsync( + _byteBuffer, + 0, + _byteBufferSize).ConfigureAwait(false); + if (_bytesRead == 0) + { + // We're at EOF + return _charsRead; + } + + // _isBlocked == whether we read fewer bytes than we asked for. + _isBlocked = (_bytesRead < _byteBufferSize); + + _charsRead += _decoder.GetChars( + _byteBuffer, + 0, + _bytesRead, + _charBuffer, + _charsRead); + } + while (_charsRead == 0); + + return _charsRead; + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs b/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs new file mode 100644 index 0000000000000000000000000000000000000000..050088ccb735b3c221b45d0db255779b54924ade --- /dev/null +++ b/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs @@ -0,0 +1,340 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// <summary> + /// Writes to the <see cref="Stream"/> using the supplied <see cref="Encoding"/>. + /// It does not write the BOM and also does not close the stream. + /// </summary> + public class HttpResponseStreamWriter : TextWriter + { + private const int MinBufferSize = 128; + internal const int DefaultBufferSize = 16 * 1024; + + private Stream _stream; + private readonly Encoder _encoder; + private readonly ArrayPool<byte> _bytePool; + private readonly ArrayPool<char> _charPool; + private readonly int _charBufferSize; + + private byte[] _byteBuffer; + private char[] _charBuffer; + + private int _charBufferCount; + private bool _disposed; + + public HttpResponseStreamWriter(Stream stream, Encoding encoding) + : this(stream, encoding, DefaultBufferSize, ArrayPool<byte>.Shared, ArrayPool<char>.Shared) + { + } + + public HttpResponseStreamWriter(Stream stream, Encoding encoding, int bufferSize) + : this(stream, encoding, bufferSize, ArrayPool<byte>.Shared, ArrayPool<char>.Shared) + { + } + + public HttpResponseStreamWriter( + Stream stream, + Encoding encoding, + int bufferSize, + ArrayPool<byte> bytePool, + ArrayPool<char> charPool) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + Encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); + _bytePool = bytePool ?? throw new ArgumentNullException(nameof(bytePool)); + _charPool = charPool ?? throw new ArgumentNullException(nameof(charPool)); + + if (bufferSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + if (!_stream.CanWrite) + { + throw new ArgumentException(Resources.HttpResponseStreamWriter_StreamNotWritable, nameof(stream)); + } + + _charBufferSize = bufferSize; + + _encoder = encoding.GetEncoder(); + _charBuffer = charPool.Rent(bufferSize); + + try + { + var requiredLength = encoding.GetMaxByteCount(bufferSize); + _byteBuffer = bytePool.Rent(requiredLength); + } + catch + { + charPool.Return(_charBuffer); + + if (_byteBuffer != null) + { + bytePool.Return(_byteBuffer); + } + + throw; + } + } + + public override Encoding Encoding { get; } + + public override void Write(char value) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + if (_charBufferCount == _charBufferSize) + { + FlushInternal(flushEncoder: false); + } + + _charBuffer[_charBufferCount] = value; + _charBufferCount++; + } + + public override void Write(char[] values, int index, int count) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + if (values == null) + { + return; + } + + while (count > 0) + { + if (_charBufferCount == _charBufferSize) + { + FlushInternal(flushEncoder: false); + } + + CopyToCharBuffer(values, ref index, ref count); + } + } + + public override void Write(string value) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + if (value == null) + { + return; + } + + var count = value.Length; + var index = 0; + while (count > 0) + { + if (_charBufferCount == _charBufferSize) + { + FlushInternal(flushEncoder: false); + } + + CopyToCharBuffer(value, ref index, ref count); + } + } + + public override async Task WriteAsync(char value) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + if (_charBufferCount == _charBufferSize) + { + await FlushInternalAsync(flushEncoder: false); + } + + _charBuffer[_charBufferCount] = value; + _charBufferCount++; + } + + public override async Task WriteAsync(char[] values, int index, int count) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + if (values == null) + { + return; + } + + while (count > 0) + { + if (_charBufferCount == _charBufferSize) + { + await FlushInternalAsync(flushEncoder: false); + } + + CopyToCharBuffer(values, ref index, ref count); + } + } + + public override async Task WriteAsync(string value) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + if (value == null) + { + return; + } + + var count = value.Length; + var index = 0; + while (count > 0) + { + if (_charBufferCount == _charBufferSize) + { + await FlushInternalAsync(flushEncoder: false); + } + + CopyToCharBuffer(value, ref index, ref count); + } + } + + // We want to flush the stream when Flush/FlushAsync is explicitly + // called by the user (example: from a Razor view). + + public override void Flush() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + FlushInternal(flushEncoder: true); + } + + public override Task FlushAsync() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + return FlushInternalAsync(flushEncoder: true); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + try + { + FlushInternal(flushEncoder: true); + } + finally + { + _bytePool.Return(_byteBuffer); + _charPool.Return(_charBuffer); + } + } + + base.Dispose(disposing); + } + + // Note: our FlushInternal method does NOT flush the underlying stream. This would result in + // chunking. + private void FlushInternal(bool flushEncoder) + { + if (_charBufferCount == 0) + { + return; + } + + var count = _encoder.GetBytes( + _charBuffer, + 0, + _charBufferCount, + _byteBuffer, + 0, + flush: flushEncoder); + + _charBufferCount = 0; + + if (count > 0) + { + _stream.Write(_byteBuffer, 0, count); + } + } + + // Note: our FlushInternalAsync method does NOT flush the underlying stream. This would result in + // chunking. + private async Task FlushInternalAsync(bool flushEncoder) + { + if (_charBufferCount == 0) + { + return; + } + + var count = _encoder.GetBytes( + _charBuffer, + 0, + _charBufferCount, + _byteBuffer, + 0, + flush: flushEncoder); + + _charBufferCount = 0; + + if (count > 0) + { + await _stream.WriteAsync(_byteBuffer, 0, count); + } + } + + private void CopyToCharBuffer(string value, ref int index, ref int count) + { + var remaining = Math.Min(_charBufferSize - _charBufferCount, count); + + value.CopyTo( + sourceIndex: index, + destination: _charBuffer, + destinationIndex: _charBufferCount, + count: remaining); + + _charBufferCount += remaining; + index += remaining; + count -= remaining; + } + + private void CopyToCharBuffer(char[] values, ref int index, ref int count) + { + var remaining = Math.Min(_charBufferSize - _charBufferCount, count); + + Buffer.BlockCopy( + src: values, + srcOffset: index * sizeof(char), + dst: _charBuffer, + dstOffset: _charBufferCount * sizeof(char), + count: remaining * sizeof(char)); + + _charBufferCount += remaining; + index += remaining; + count -= remaining; + } + } +} diff --git a/src/Http/WebUtilities/src/KeyValueAccumulator.cs b/src/Http/WebUtilities/src/KeyValueAccumulator.cs new file mode 100644 index 0000000000000000000000000000000000000000..5ae402e5236cbdd721260de9d5d06875cec49f50 --- /dev/null +++ b/src/Http/WebUtilities/src/KeyValueAccumulator.cs @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public struct KeyValueAccumulator + { + private Dictionary<string, StringValues> _accumulator; + private Dictionary<string, List<string>> _expandingAccumulator; + + public void Append(string key, string value) + { + if (_accumulator == null) + { + _accumulator = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase); + } + + StringValues values; + if (_accumulator.TryGetValue(key, out values)) + { + if (values.Count == 0) + { + // Marker entry for this key to indicate entry already in expanding list dictionary + _expandingAccumulator[key].Add(value); + } + else if (values.Count == 1) + { + // Second value for this key + _accumulator[key] = new string[] { values[0], value }; + } + else + { + // Third value for this key + // Add zero count entry and move to data to expanding list dictionary + _accumulator[key] = default(StringValues); + + if (_expandingAccumulator == null) + { + _expandingAccumulator = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); + } + + // Already 3 entries so use starting allocated as 8; then use List's expansion mechanism for more + var list = new List<string>(8); + var array = values.ToArray(); + + list.Add(array[0]); + list.Add(array[1]); + list.Add(value); + + _expandingAccumulator[key] = list; + } + } + else + { + // First value for this key + _accumulator[key] = new StringValues(value); + } + + ValueCount++; + } + + public bool HasValues => ValueCount > 0; + + public int KeyCount => _accumulator?.Count ?? 0; + + public int ValueCount { get; private set; } + + public Dictionary<string, StringValues> GetResults() + { + if (_expandingAccumulator != null) + { + // Coalesce count 3+ multi-value entries into _accumulator dictionary + foreach (var entry in _expandingAccumulator) + { + _accumulator[entry.Key] = new StringValues(entry.Value.ToArray()); + } + } + + return _accumulator ?? new Dictionary<string, StringValues>(0, StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/src/Http/WebUtilities/src/Microsoft.AspNetCore.WebUtilities.csproj b/src/Http/WebUtilities/src/Microsoft.AspNetCore.WebUtilities.csproj new file mode 100644 index 0000000000000000000000000000000000000000..3c7d2d8255b63273d6fe4877acf4f991c6d21de5 --- /dev/null +++ b/src/Http/WebUtilities/src/Microsoft.AspNetCore.WebUtilities.csproj @@ -0,0 +1,18 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>ASP.NET Core utilities, such as for working with forms, multipart messages, and query strings.</Description> + <TargetFramework>netstandard2.0</TargetFramework> + <DefineConstants>$(DefineConstants);WebEncoders_In_WebUtilities</DefineConstants> + <NoWarn>$(NoWarn);CS1591</NoWarn> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore</PackageTags> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.Extensions.WebEncoders.Sources" PrivateAssets="All" /> + <Reference Include="Microsoft.Net.Http.Headers" /> + <Reference Include="System.Text.Encodings.Web" /> + </ItemGroup> + +</Project> diff --git a/src/Http/WebUtilities/src/MultipartBoundary.cs b/src/Http/WebUtilities/src/MultipartBoundary.cs new file mode 100644 index 0000000000000000000000000000000000000000..0da13038353f0e2d82f683a6577cb9816a23e8c9 --- /dev/null +++ b/src/Http/WebUtilities/src/MultipartBoundary.cs @@ -0,0 +1,72 @@ +// 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.Text; + +namespace Microsoft.AspNetCore.WebUtilities +{ + internal class MultipartBoundary + { + private readonly int[] _skipTable = new int[256]; + private readonly string _boundary; + private bool _expectLeadingCrlf; + + public MultipartBoundary(string boundary, bool expectLeadingCrlf = true) + { + if (boundary == null) + { + throw new ArgumentNullException(nameof(boundary)); + } + + _boundary = boundary; + _expectLeadingCrlf = expectLeadingCrlf; + Initialize(_boundary, _expectLeadingCrlf); + } + + private void Initialize(string boundary, bool expectLeadingCrlf) + { + if (expectLeadingCrlf) + { + BoundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary); + } + else + { + BoundaryBytes = Encoding.UTF8.GetBytes("--" + boundary); + } + FinalBoundaryLength = BoundaryBytes.Length + 2; // Include the final '--' terminator. + + var length = BoundaryBytes.Length; + for (var i = 0; i < _skipTable.Length; ++i) + { + _skipTable[i] = length; + } + for (var i = 0; i < length; ++i) + { + _skipTable[BoundaryBytes[i]] = Math.Max(1, length - 1 - i); + } + } + + public int GetSkipValue(byte input) + { + return _skipTable[input]; + } + + public bool ExpectLeadingCrlf + { + get { return _expectLeadingCrlf; } + set + { + if (value != _expectLeadingCrlf) + { + _expectLeadingCrlf = value; + Initialize(_boundary, _expectLeadingCrlf); + } + } + } + + public byte[] BoundaryBytes { get; private set; } + + public int FinalBoundaryLength { get; private set; } + } +} diff --git a/src/Http/WebUtilities/src/MultipartReader.cs b/src/Http/WebUtilities/src/MultipartReader.cs new file mode 100644 index 0000000000000000000000000000000000000000..2da50a53600b8b49cea86a9403034f8ae2a4dd20 --- /dev/null +++ b/src/Http/WebUtilities/src/MultipartReader.cs @@ -0,0 +1,118 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.WebUtilities +{ + // https://www.ietf.org/rfc/rfc2046.txt + public class MultipartReader + { + public const int DefaultHeadersCountLimit = 16; + public const int DefaultHeadersLengthLimit = 1024 * 16; + private const int DefaultBufferSize = 1024 * 4; + + private readonly BufferedReadStream _stream; + private readonly MultipartBoundary _boundary; + private MultipartReaderStream _currentStream; + + public MultipartReader(string boundary, Stream stream) + : this(boundary, stream, DefaultBufferSize) + { + } + + public MultipartReader(string boundary, Stream stream, int bufferSize) + { + if (boundary == null) + { + throw new ArgumentNullException(nameof(boundary)); + } + + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (bufferSize < boundary.Length + 8) // Size of the boundary + leading and trailing CRLF + leading and trailing '--' markers. + { + throw new ArgumentOutOfRangeException(nameof(bufferSize), bufferSize, "Insufficient buffer space, the buffer must be larger than the boundary: " + boundary); + } + _stream = new BufferedReadStream(stream, bufferSize); + _boundary = new MultipartBoundary(boundary, false); + // This stream will drain any preamble data and remove the first boundary marker. + // TODO: HeadersLengthLimit can't be modified until after the constructor. + _currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = HeadersLengthLimit }; + } + + /// <summary> + /// The limit for the number of headers to read. + /// </summary> + public int HeadersCountLimit { get; set; } = DefaultHeadersCountLimit; + + /// <summary> + /// The combined size limit for headers per multipart section. + /// </summary> + public int HeadersLengthLimit { get; set; } = DefaultHeadersLengthLimit; + + /// <summary> + /// The optional limit for the total response body length. + /// </summary> + public long? BodyLengthLimit { get; set; } + + public async Task<MultipartSection> ReadNextSectionAsync(CancellationToken cancellationToken = new CancellationToken()) + { + // Drain the prior section. + await _currentStream.DrainAsync(cancellationToken); + // If we're at the end return null + if (_currentStream.FinalBoundaryFound) + { + // There may be trailer data after the last boundary. + await _stream.DrainAsync(HeadersLengthLimit, cancellationToken); + return null; + } + var headers = await ReadHeadersAsync(cancellationToken); + _boundary.ExpectLeadingCrlf = true; + _currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = BodyLengthLimit }; + long? baseStreamOffset = _stream.CanSeek ? (long?)_stream.Position : null; + return new MultipartSection() { Headers = headers, Body = _currentStream, BaseStreamOffset = baseStreamOffset }; + } + + private async Task<Dictionary<string, StringValues>> ReadHeadersAsync(CancellationToken cancellationToken) + { + int totalSize = 0; + var accumulator = new KeyValueAccumulator(); + var line = await _stream.ReadLineAsync(HeadersLengthLimit - totalSize, cancellationToken); + while (!string.IsNullOrEmpty(line)) + { + if (HeadersLengthLimit - totalSize < line.Length) + { + throw new InvalidDataException($"Multipart headers length limit {HeadersLengthLimit} exceeded."); + } + totalSize += line.Length; + int splitIndex = line.IndexOf(':'); + if (splitIndex <= 0) + { + throw new InvalidDataException($"Invalid header line: {line}"); + } + + var name = line.Substring(0, splitIndex); + var value = line.Substring(splitIndex + 1, line.Length - splitIndex - 1).Trim(); + accumulator.Append(name, value); + if (accumulator.KeyCount > HeadersCountLimit) + { + throw new InvalidDataException($"Multipart headers count limit {HeadersCountLimit} exceeded."); + } + + line = await _stream.ReadLineAsync(HeadersLengthLimit - totalSize, cancellationToken); + } + + return accumulator.GetResults(); + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/MultipartReaderStream.cs b/src/Http/WebUtilities/src/MultipartReaderStream.cs new file mode 100644 index 0000000000000000000000000000000000000000..7952bd34b2299824d191cd7f2268bf145abbee41 --- /dev/null +++ b/src/Http/WebUtilities/src/MultipartReaderStream.cs @@ -0,0 +1,336 @@ +// 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.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebUtilities +{ + internal class MultipartReaderStream : Stream + { + private readonly MultipartBoundary _boundary; + private readonly BufferedReadStream _innerStream; + private readonly ArrayPool<byte> _bytePool; + + private readonly long _innerOffset; + private long _position; + private long _observedLength; + private bool _finished; + + /// <summary> + /// Creates a stream that reads until it reaches the given boundary pattern. + /// </summary> + /// <param name="stream">The <see cref="BufferedReadStream"/>.</param> + /// <param name="boundary">The boundary pattern to use.</param> + public MultipartReaderStream(BufferedReadStream stream, MultipartBoundary boundary) + : this(stream, boundary, ArrayPool<byte>.Shared) + { + } + + /// <summary> + /// Creates a stream that reads until it reaches the given boundary pattern. + /// </summary> + /// <param name="stream">The <see cref="BufferedReadStream"/>.</param> + /// <param name="boundary">The boundary pattern to use.</param> + /// <param name="bytePool">The ArrayPool pool to use for temporary byte arrays.</param> + public MultipartReaderStream(BufferedReadStream stream, MultipartBoundary boundary, ArrayPool<byte> bytePool) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (boundary == null) + { + throw new ArgumentNullException(nameof(boundary)); + } + + _bytePool = bytePool; + _innerStream = stream; + _innerOffset = _innerStream.CanSeek ? _innerStream.Position : 0; + _boundary = boundary; + } + + public bool FinalBoundaryFound { get; private set; } + + public long? LengthLimit { get; set; } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return _innerStream.CanSeek; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _observedLength; } + } + + public override long Position + { + get { return _position; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "The Position must be positive."); + } + if (value > _observedLength) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "The Position must be less than length."); + } + _position = value; + if (_position < _observedLength) + { + _finished = false; + } + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + Position = offset; + } + else if (origin == SeekOrigin.Current) + { + Position = Position + offset; + } + else // if (origin == SeekOrigin.End) + { + Position = Length + offset; + } + return Position; + } + + public override void SetLength(long value) + { + 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(); + } + + private void PositionInnerStream() + { + if (_innerStream.CanSeek && _innerStream.Position != (_innerOffset + _position)) + { + _innerStream.Position = _innerOffset + _position; + } + } + + private int UpdatePosition(int read) + { + _position += read; + if (_observedLength < _position) + { + _observedLength = _position; + if (LengthLimit.HasValue && _observedLength > LengthLimit.Value) + { + throw new InvalidDataException($"Multipart body length limit {LengthLimit.Value} exceeded."); + } + } + return read; + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (_finished) + { + return 0; + } + + PositionInnerStream(); + if (!_innerStream.EnsureBuffered(_boundary.FinalBoundaryLength)) + { + throw new IOException("Unexpected end of Stream, the content may have already been read by another component. "); + } + var bufferedData = _innerStream.BufferedData; + + // scan for a boundary match, full or partial. + int read; + if (SubMatch(bufferedData, _boundary.BoundaryBytes, out var matchOffset, out var matchCount)) + { + // We found a possible match, return any data before it. + if (matchOffset > bufferedData.Offset) + { + read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset)); + return UpdatePosition(read); + } + + var length = _boundary.BoundaryBytes.Length; + Debug.Assert(matchCount == length); + + // "The boundary may be followed by zero or more characters of + // linear whitespace. It is then terminated by either another CRLF" + // or -- for the final boundary. + var boundary = _bytePool.Rent(length); + read = _innerStream.Read(boundary, 0, length); + _bytePool.Return(boundary); + Debug.Assert(read == length); // It should have all been buffered + + var remainder = _innerStream.ReadLine(lengthLimit: 100); // Whitespace may exceed the buffer. + remainder = remainder.Trim(); + if (string.Equals("--", remainder, StringComparison.Ordinal)) + { + FinalBoundaryFound = true; + } + Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); + _finished = true; + return 0; + } + + // No possible boundary match within the buffered data, return the data from the buffer. + read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count)); + return UpdatePosition(read); + } + + public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_finished) + { + return 0; + } + + PositionInnerStream(); + if (!await _innerStream.EnsureBufferedAsync(_boundary.FinalBoundaryLength, cancellationToken)) + { + throw new IOException("Unexpected end of Stream, the content may have already been read by another component. "); + } + var bufferedData = _innerStream.BufferedData; + + // scan for a boundary match, full or partial. + int matchOffset; + int matchCount; + int read; + if (SubMatch(bufferedData, _boundary.BoundaryBytes, out matchOffset, out matchCount)) + { + // We found a possible match, return any data before it. + if (matchOffset > bufferedData.Offset) + { + // Sync, it's already buffered + read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset)); + return UpdatePosition(read); + } + + var length = _boundary.BoundaryBytes.Length; + Debug.Assert(matchCount == length); + + // "The boundary may be followed by zero or more characters of + // linear whitespace. It is then terminated by either another CRLF" + // or -- for the final boundary. + var boundary = _bytePool.Rent(length); + read = _innerStream.Read(boundary, 0, length); + _bytePool.Return(boundary); + Debug.Assert(read == length); // It should have all been buffered + + var remainder = await _innerStream.ReadLineAsync(lengthLimit: 100, cancellationToken: cancellationToken); // Whitespace may exceed the buffer. + remainder = remainder.Trim(); + if (string.Equals("--", remainder, StringComparison.Ordinal)) + { + FinalBoundaryFound = true; + } + Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); + + _finished = true; + return 0; + } + + // No possible boundary match within the buffered data, return the data from the buffer. + read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count)); + return UpdatePosition(read); + } + + // Does segment1 contain all of matchBytes, or does it end with the start of matchBytes? + // 1: AAAAABBBBBCCCCC + // 2: BBBBB + // Or: + // 1: AAAAABBB + // 2: BBBBB + private bool SubMatch(ArraySegment<byte> segment1, byte[] matchBytes, out int matchOffset, out int matchCount) + { + // clear matchCount to zero + matchCount = 0; + + // case 1: does segment1 fully contain matchBytes? + { + var matchBytesLengthMinusOne = matchBytes.Length - 1; + var matchBytesLastByte = matchBytes[matchBytesLengthMinusOne]; + var segmentEndMinusMatchBytesLength = segment1.Offset + segment1.Count - matchBytes.Length; + + matchOffset = segment1.Offset; + while (matchOffset < segmentEndMinusMatchBytesLength) + { + var lookaheadTailChar = segment1.Array[matchOffset + matchBytesLengthMinusOne]; + if (lookaheadTailChar == matchBytesLastByte && + CompareBuffers(segment1.Array, matchOffset, matchBytes, 0, matchBytesLengthMinusOne) == 0) + { + matchCount = matchBytes.Length; + return true; + } + matchOffset += _boundary.GetSkipValue(lookaheadTailChar); + } + } + + // case 2: does segment1 end with the start of matchBytes? + var segmentEnd = segment1.Offset + segment1.Count; + + matchCount = 0; + for (; matchOffset < segmentEnd; matchOffset++) + { + var countLimit = segmentEnd - matchOffset; + for (matchCount = 0; matchCount < matchBytes.Length && matchCount < countLimit; matchCount++) + { + if (matchBytes[matchCount] != segment1.Array[matchOffset + matchCount]) + { + matchCount = 0; + break; + } + } + if (matchCount > 0) + { + break; + } + } + return matchCount > 0; + } + + private static int CompareBuffers(byte[] buffer1, int offset1, byte[] buffer2, int offset2, int count) + { + for (; count-- > 0; offset1++, offset2++) + { + if (buffer1[offset1] != buffer2[offset2]) + { + return buffer1[offset1] - buffer2[offset2]; + } + } + return 0; + } + } +} diff --git a/src/Http/WebUtilities/src/MultipartSection.cs b/src/Http/WebUtilities/src/MultipartSection.cs new file mode 100644 index 0000000000000000000000000000000000000000..96138c630a10e58edbdbb8857d3714a207703ed6 --- /dev/null +++ b/src/Http/WebUtilities/src/MultipartSection.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class MultipartSection + { + public string ContentType + { + get + { + StringValues values; + if (Headers.TryGetValue("Content-Type", out values)) + { + return values; + } + return null; + } + } + + public string ContentDisposition + { + get + { + StringValues values; + if (Headers.TryGetValue("Content-Disposition", out values)) + { + return values; + } + return null; + } + } + + public Dictionary<string, StringValues> Headers { get; set; } + + public Stream Body { get; set; } + + /// <summary> + /// The position where the body starts in the total multipart body. + /// This may not be available if the total multipart body is not seekable. + /// </summary> + public long? BaseStreamOffset { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/MultipartSectionConverterExtensions.cs b/src/Http/WebUtilities/src/MultipartSectionConverterExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..826ced168eaa966de182aba9278dbf3cf2a6d32d --- /dev/null +++ b/src/Http/WebUtilities/src/MultipartSectionConverterExtensions.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// <summary> + /// Various extensions for converting multipart sections + /// </summary> + public static class MultipartSectionConverterExtensions + { + /// <summary> + /// Converts the section to a file section + /// </summary> + /// <param name="section">The section to convert</param> + /// <returns>A file section</returns> + public static FileMultipartSection AsFileSection(this MultipartSection section) + { + if (section == null) + { + throw new ArgumentNullException(nameof(section)); + } + + try + { + return new FileMultipartSection(section); + } + catch + { + return null; + } + } + + /// <summary> + /// Converts the section to a form section + /// </summary> + /// <param name="section">The section to convert</param> + /// <returns>A form section</returns> + public static FormMultipartSection AsFormDataSection(this MultipartSection section) + { + if (section == null) + { + throw new ArgumentNullException(nameof(section)); + } + + try + { + return new FormMultipartSection(section); + } + catch + { + return null; + } + } + + /// <summary> + /// Retrieves and parses the content disposition header from a section + /// </summary> + /// <param name="section">The section from which to retrieve</param> + /// <returns>A <see cref="ContentDispositionHeaderValue"/> if the header was found, null otherwise</returns> + public static ContentDispositionHeaderValue GetContentDispositionHeader(this MultipartSection section) + { + ContentDispositionHeaderValue header; + if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out header)) + { + return null; + } + + return header; + } + } +} diff --git a/src/Http/WebUtilities/src/MultipartSectionStreamExtensions.cs b/src/Http/WebUtilities/src/MultipartSectionStreamExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..463a8d88d6ab8ada047592820a77a54c790aa1e4 --- /dev/null +++ b/src/Http/WebUtilities/src/MultipartSectionStreamExtensions.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// <summary> + /// Various extension methods for dealing with the section body stream + /// </summary> + public static class MultipartSectionStreamExtensions + { + /// <summary> + /// Reads the body of the section as a string + /// </summary> + /// <param name="section">The section to read from</param> + /// <returns>The body steam as string</returns> + public static async Task<string> ReadAsStringAsync(this MultipartSection section) + { + if (section == null) + { + throw new ArgumentNullException(nameof(section)); + } + + MediaTypeHeaderValue sectionMediaType; + MediaTypeHeaderValue.TryParse(section.ContentType, out sectionMediaType); + + Encoding streamEncoding = sectionMediaType?.Encoding; + if (streamEncoding == null || streamEncoding == Encoding.UTF7) + { + streamEncoding = Encoding.UTF8; + } + + using (var reader = new StreamReader( + section.Body, + streamEncoding, + detectEncodingFromByteOrderMarks: true, + bufferSize: 1024, + leaveOpen: true)) + { + return await reader.ReadToEndAsync(); + } + } + } +} diff --git a/src/Http/WebUtilities/src/Properties/AssemblyInfo.cs b/src/Http/WebUtilities/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..aa80ef1d7e9fdae5987f7fc743c50d694a26e9d0 --- /dev/null +++ b/src/Http/WebUtilities/src/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.WebUtilities.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Http/WebUtilities/src/QueryHelpers.cs b/src/Http/WebUtilities/src/QueryHelpers.cs new file mode 100644 index 0000000000000000000000000000000000000000..6bd1a0bb82eecd239713d975ccd70ebc1abe8d72 --- /dev/null +++ b/src/Http/WebUtilities/src/QueryHelpers.cs @@ -0,0 +1,191 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Encodings.Web; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public static class QueryHelpers + { + /// <summary> + /// Append the given query key and value to the URI. + /// </summary> + /// <param name="uri">The base URI.</param> + /// <param name="name">The name of the query key.</param> + /// <param name="value">The query value.</param> + /// <returns>The combined result.</returns> + public static string AddQueryString(string uri, string name, string value) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + return AddQueryString( + uri, new[] { new KeyValuePair<string, string>(name, value) }); + } + + /// <summary> + /// Append the given query keys and values to the uri. + /// </summary> + /// <param name="uri">The base uri.</param> + /// <param name="queryString">A collection of name value query pairs to append.</param> + /// <returns>The combined result.</returns> + public static string AddQueryString(string uri, IDictionary<string, string> queryString) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (queryString == null) + { + throw new ArgumentNullException(nameof(queryString)); + } + + return AddQueryString(uri, (IEnumerable<KeyValuePair<string, string>>)queryString); + } + + private static string AddQueryString( + string uri, + IEnumerable<KeyValuePair<string, string>> queryString) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (queryString == null) + { + throw new ArgumentNullException(nameof(queryString)); + } + + var anchorIndex = uri.IndexOf('#'); + var uriToBeAppended = uri; + var anchorText = ""; + // If there is an anchor, then the query string must be inserted before its first occurance. + if (anchorIndex != -1) + { + anchorText = uri.Substring(anchorIndex); + uriToBeAppended = uri.Substring(0, anchorIndex); + } + + var queryIndex = uriToBeAppended.IndexOf('?'); + var hasQuery = queryIndex != -1; + + var sb = new StringBuilder(); + sb.Append(uriToBeAppended); + foreach (var parameter in queryString) + { + sb.Append(hasQuery ? '&' : '?'); + sb.Append(UrlEncoder.Default.Encode(parameter.Key)); + sb.Append('='); + sb.Append(UrlEncoder.Default.Encode(parameter.Value)); + hasQuery = true; + } + + sb.Append(anchorText); + return sb.ToString(); + } + + /// <summary> + /// Parse a query string into its component key and value parts. + /// </summary> + /// <param name="queryString">The raw query string value, with or without the leading '?'.</param> + /// <returns>A collection of parsed keys and values.</returns> + public static Dictionary<string, StringValues> ParseQuery(string queryString) + { + var result = ParseNullableQuery(queryString); + + if (result == null) + { + return new Dictionary<string, StringValues>(); + } + + return result; + } + + + /// <summary> + /// Parse a query string into its component key and value parts. + /// </summary> + /// <param name="queryString">The raw query string value, with or without the leading '?'.</param> + /// <returns>A collection of parsed keys and values, null if there are no entries.</returns> + public static Dictionary<string, StringValues> ParseNullableQuery(string queryString) + { + var accumulator = new KeyValueAccumulator(); + + if (string.IsNullOrEmpty(queryString) || queryString == "?") + { + return null; + } + + int scanIndex = 0; + if (queryString[0] == '?') + { + scanIndex = 1; + } + + int textLength = queryString.Length; + int equalIndex = queryString.IndexOf('='); + if (equalIndex == -1) + { + equalIndex = textLength; + } + while (scanIndex < textLength) + { + int delimiterIndex = queryString.IndexOf('&', scanIndex); + if (delimiterIndex == -1) + { + delimiterIndex = textLength; + } + if (equalIndex < delimiterIndex) + { + while (scanIndex != equalIndex && char.IsWhiteSpace(queryString[scanIndex])) + { + ++scanIndex; + } + string name = queryString.Substring(scanIndex, equalIndex - scanIndex); + string value = queryString.Substring(equalIndex + 1, delimiterIndex - equalIndex - 1); + accumulator.Append( + Uri.UnescapeDataString(name.Replace('+', ' ')), + Uri.UnescapeDataString(value.Replace('+', ' '))); + equalIndex = queryString.IndexOf('=', delimiterIndex); + if (equalIndex == -1) + { + equalIndex = textLength; + } + } + else + { + if (delimiterIndex > scanIndex) + { + accumulator.Append(queryString.Substring(scanIndex, delimiterIndex - scanIndex), string.Empty); + } + } + scanIndex = delimiterIndex + 1; + } + + if (!accumulator.HasValues) + { + return null; + } + + return accumulator.GetResults(); + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/ReasonPhrases.cs b/src/Http/WebUtilities/src/ReasonPhrases.cs new file mode 100644 index 0000000000000000000000000000000000000000..3aab17079d0c21d1f89294a9aa2a91991a7a4a0c --- /dev/null +++ b/src/Http/WebUtilities/src/ReasonPhrases.cs @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public static class ReasonPhrases + { + // Status Codes listed at http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + private static IDictionary<int, string> Phrases = new Dictionary<int, string>() + { + { 100, "Continue" }, + { 101, "Switching Protocols" }, + { 102, "Processing" }, + + { 200, "OK" }, + { 201, "Created" }, + { 202, "Accepted" }, + { 203, "Non-Authoritative Information" }, + { 204, "No Content" }, + { 205, "Reset Content" }, + { 206, "Partial Content" }, + { 207, "Multi-Status" }, + { 208, "Already Reported" }, + { 226, "IM Used" }, + + { 300, "Multiple Choices" }, + { 301, "Moved Permanently" }, + { 302, "Found" }, + { 303, "See Other" }, + { 304, "Not Modified" }, + { 305, "Use Proxy" }, + { 306, "Switch Proxy" }, + { 307, "Temporary Redirect" }, + { 308, "Permanent Redirect" }, + + { 400, "Bad Request" }, + { 401, "Unauthorized" }, + { 402, "Payment Required" }, + { 403, "Forbidden" }, + { 404, "Not Found" }, + { 405, "Method Not Allowed" }, + { 406, "Not Acceptable" }, + { 407, "Proxy Authentication Required" }, + { 408, "Request Timeout" }, + { 409, "Conflict" }, + { 410, "Gone" }, + { 411, "Length Required" }, + { 412, "Precondition Failed" }, + { 413, "Payload Too Large" }, + { 414, "URI Too Long" }, + { 415, "Unsupported Media Type" }, + { 416, "Range Not Satisfiable" }, + { 417, "Expectation Failed" }, + { 418, "I'm a teapot" }, + { 419, "Authentication Timeout" }, + { 421, "Misdirected Request" }, + { 422, "Unprocessable Entity" }, + { 423, "Locked" }, + { 424, "Failed Dependency" }, + { 426, "Upgrade Required" }, + { 428, "Precondition Required" }, + { 429, "Too Many Requests" }, + { 431, "Request Header Fields Too Large" }, + { 451, "Unavailable For Legal Reasons" }, + + { 500, "Internal Server Error" }, + { 501, "Not Implemented" }, + { 502, "Bad Gateway" }, + { 503, "Service Unavailable" }, + { 504, "Gateway Timeout" }, + { 505, "HTTP Version Not Supported" }, + { 506, "Variant Also Negotiates" }, + { 507, "Insufficient Storage" }, + { 508, "Loop Detected" }, + { 510, "Not Extended" }, + { 511, "Network Authentication Required" }, + }; + + public static string GetReasonPhrase(int statusCode) + { + string phrase; + return Phrases.TryGetValue(statusCode, out phrase) ? phrase : string.Empty; + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/Resources.Designer.cs b/src/Http/WebUtilities/src/Resources.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..7972e005d03b53032dbc86dfd9228058c31c555f --- /dev/null +++ b/src/Http/WebUtilities/src/Resources.Designer.cs @@ -0,0 +1,89 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNetCore.WebUtilities { + using System; + using System.Reflection; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + internal Resources() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNetCore.WebUtilities.Resources", typeof(Resources).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to The stream must support reading.. + /// </summary> + internal static string HttpRequestStreamReader_StreamNotReadable { + get { + return ResourceManager.GetString("HttpRequestStreamReader_StreamNotReadable", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The stream must support writing.. + /// </summary> + internal static string HttpResponseStreamWriter_StreamNotWritable { + get { + return ResourceManager.GetString("HttpResponseStreamWriter_StreamNotWritable", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Invalid {0}, {1} or {2} length.. + /// </summary> + internal static string WebEncoders_InvalidCountOffsetOrLength { + get { + return ResourceManager.GetString("WebEncoders_InvalidCountOffsetOrLength", resourceCulture); + } + } + } +} diff --git a/src/Http/WebUtilities/src/Resources.resx b/src/Http/WebUtilities/src/Resources.resx new file mode 100644 index 0000000000000000000000000000000000000000..a32d2db5cc6095aed8aa96d716d4e4580741b3ca --- /dev/null +++ b/src/Http/WebUtilities/src/Resources.resx @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="HttpRequestStreamReader_StreamNotReadable" xml:space="preserve"> + <value>The stream must support reading.</value> + </data> + <data name="HttpResponseStreamWriter_StreamNotWritable" xml:space="preserve"> + <value>The stream must support writing.</value> + </data> + <data name="WebEncoders_InvalidCountOffsetOrLength" xml:space="preserve"> + <value>Invalid {0}, {1} or {2} length.</value> + </data> +</root> \ No newline at end of file diff --git a/src/Http/WebUtilities/src/StreamHelperExtensions.cs b/src/Http/WebUtilities/src/StreamHelperExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..e2c16a9cf234ca6d8908a009d97ef7a95705d007 --- /dev/null +++ b/src/Http/WebUtilities/src/StreamHelperExtensions.cs @@ -0,0 +1,51 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public static class StreamHelperExtensions + { + private const int _maxReadBufferSize = 1024 * 4; + + public static Task DrainAsync(this Stream stream, CancellationToken cancellationToken) + { + return stream.DrainAsync(ArrayPool<byte>.Shared, null, cancellationToken); + } + + public static Task DrainAsync(this Stream stream, long? limit, CancellationToken cancellationToken) + { + return stream.DrainAsync(ArrayPool<byte>.Shared, limit, cancellationToken); + } + + public static async Task DrainAsync(this Stream stream, ArrayPool<byte> bytePool, long? limit, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var buffer = bytePool.Rent(_maxReadBufferSize); + long total = 0; + try + { + var read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken); + while (read > 0) + { + // Not all streams support cancellation directly. + cancellationToken.ThrowIfCancellationRequested(); + if (limit.HasValue && limit.Value - total < read) + { + throw new InvalidDataException($"The stream exceeded the data limit {limit.Value}."); + } + total += read; + read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken); + } + } + finally + { + bytePool.Return(buffer); + } + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/baseline.netcore.json b/src/Http/WebUtilities/src/baseline.netcore.json new file mode 100644 index 0000000000000000000000000000000000000000..896fe0fcb3508bbf2adb40dc99ee87e7284d70ce --- /dev/null +++ b/src/Http/WebUtilities/src/baseline.netcore.json @@ -0,0 +1,2272 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.WebUtilities, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.WebUtilities.WebEncoders", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Base64UrlDecode", + "Parameters": [ + { + "Name": "input", + "Type": "System.String" + } + ], + "ReturnType": "System.Byte[]", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Base64UrlDecode", + "Parameters": [ + { + "Name": "input", + "Type": "System.String" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Byte[]", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Base64UrlDecode", + "Parameters": [ + { + "Name": "input", + "Type": "System.String" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "buffer", + "Type": "System.Char[]" + }, + { + "Name": "bufferOffset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Byte[]", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetArraySizeRequiredToDecode", + "Parameters": [ + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Base64UrlEncode", + "Parameters": [ + { + "Name": "input", + "Type": "System.Byte[]" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Base64UrlEncode", + "Parameters": [ + { + "Name": "input", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Base64UrlEncode", + "Parameters": [ + { + "Name": "input", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "output", + "Type": "System.Char[]" + }, + { + "Name": "outputOffset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetArraySizeRequiredToEncode", + "Parameters": [ + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.Base64UrlTextEncoder", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Encode", + "Parameters": [ + { + "Name": "data", + "Type": "System.Byte[]" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Decode", + "Parameters": [ + { + "Name": "text", + "Type": "System.String" + } + ], + "ReturnType": "System.Byte[]", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.BufferedReadStream", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.IO.Stream", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_BufferedData", + "Parameters": [], + "ReturnType": "System.ArraySegment<System.Byte>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanRead", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanSeek", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanTimeout", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanWrite", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Length", + "Parameters": [], + "ReturnType": "System.Int64", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Position", + "Parameters": [], + "ReturnType": "System.Int64", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Position", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Seek", + "Parameters": [ + { + "Name": "offset", + "Type": "System.Int64" + }, + { + "Name": "origin", + "Type": "System.IO.SeekOrigin" + } + ], + "ReturnType": "System.Int64", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [ + { + "Name": "disposing", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Flush", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FlushAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteAsync", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadAsync", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Int32>", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EnsureBuffered", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EnsureBufferedAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Boolean>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EnsureBuffered", + "Parameters": [ + { + "Name": "minCount", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EnsureBufferedAsync", + "Parameters": [ + { + "Name": "minCount", + "Type": "System.Int32" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Boolean>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadLine", + "Parameters": [ + { + "Name": "lengthLimit", + "Type": "System.Int32" + } + ], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadLineAsync", + "Parameters": [ + { + "Name": "lengthLimit", + "Type": "System.Int32" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.String>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "inner", + "Type": "System.IO.Stream" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "inner", + "Type": "System.IO.Stream" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + }, + { + "Name": "bytePool", + "Type": "System.Buffers.ArrayPool<System.Byte>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.FileBufferingReadStream", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.IO.Stream", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_InMemory", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TempFileName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanRead", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanSeek", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanWrite", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Length", + "Parameters": [], + "ReturnType": "System.Int64", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Position", + "Parameters": [], + "ReturnType": "System.Int64", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Position", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Seek", + "Parameters": [ + { + "Name": "offset", + "Type": "System.Int64" + }, + { + "Name": "origin", + "Type": "System.IO.SeekOrigin" + } + ], + "ReturnType": "System.Int64", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadAsync", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Int32>", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteAsync", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Flush", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [ + { + "Name": "disposing", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "inner", + "Type": "System.IO.Stream" + }, + { + "Name": "memoryThreshold", + "Type": "System.Int32" + }, + { + "Name": "bufferLimit", + "Type": "System.Nullable<System.Int64>" + }, + { + "Name": "tempFileDirectoryAccessor", + "Type": "System.Func<System.String>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "inner", + "Type": "System.IO.Stream" + }, + { + "Name": "memoryThreshold", + "Type": "System.Int32" + }, + { + "Name": "bufferLimit", + "Type": "System.Nullable<System.Int64>" + }, + { + "Name": "tempFileDirectoryAccessor", + "Type": "System.Func<System.String>" + }, + { + "Name": "bytePool", + "Type": "System.Buffers.ArrayPool<System.Byte>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "inner", + "Type": "System.IO.Stream" + }, + { + "Name": "memoryThreshold", + "Type": "System.Int32" + }, + { + "Name": "bufferLimit", + "Type": "System.Nullable<System.Int64>" + }, + { + "Name": "tempFileDirectory", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "inner", + "Type": "System.IO.Stream" + }, + { + "Name": "memoryThreshold", + "Type": "System.Int32" + }, + { + "Name": "bufferLimit", + "Type": "System.Nullable<System.Int64>" + }, + { + "Name": "tempFileDirectory", + "Type": "System.String" + }, + { + "Name": "bytePool", + "Type": "System.Buffers.ArrayPool<System.Byte>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.FileMultipartSection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Section", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.WebUtilities.MultipartSection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FileStream", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FileName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + }, + { + "Name": "header", + "Type": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.FormMultipartSection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Section", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.WebUtilities.MultipartSection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetValueAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task<System.String>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + }, + { + "Name": "header", + "Type": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.FormReader", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.IDisposable" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ValueCountLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ValueCountLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_KeyLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_KeyLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ValueLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ValueLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadNextPair", + "Parameters": [], + "ReturnType": "System.Nullable<System.Collections.Generic.KeyValuePair<System.String, System.String>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadNextPairAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Nullable<System.Collections.Generic.KeyValuePair<System.String, System.String>>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadForm", + "Parameters": [], + "ReturnType": "System.Collections.Generic.Dictionary<System.String, Microsoft.Extensions.Primitives.StringValues>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadFormAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Collections.Generic.Dictionary<System.String, Microsoft.Extensions.Primitives.StringValues>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "data", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "data", + "Type": "System.String" + }, + { + "Name": "charPool", + "Type": "System.Buffers.ArrayPool<System.Char>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + }, + { + "Name": "charPool", + "Type": "System.Buffers.ArrayPool<System.Char>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "DefaultValueCountLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "1024" + }, + { + "Kind": "Field", + "Name": "DefaultKeyLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "2048" + }, + { + "Kind": "Field", + "Name": "DefaultValueLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "4194304" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.HttpRequestStreamReader", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.IO.TextReader", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [ + { + "Name": "disposing", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Peek", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Char[]" + }, + { + "Name": "index", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadAsync", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Char[]" + }, + { + "Name": "index", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.Int32>", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + }, + { + "Name": "bytePool", + "Type": "System.Buffers.ArrayPool<System.Byte>" + }, + { + "Name": "charPool", + "Type": "System.Buffers.ArrayPool<System.Char>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.HttpResponseStreamWriter", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.IO.TextWriter", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Encoding", + "Parameters": [], + "ReturnType": "System.Text.Encoding", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "value", + "Type": "System.Char" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "values", + "Type": "System.Char[]" + }, + { + "Name": "index", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteAsync", + "Parameters": [ + { + "Name": "value", + "Type": "System.Char" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteAsync", + "Parameters": [ + { + "Name": "values", + "Type": "System.Char[]" + }, + { + "Name": "index", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteAsync", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Flush", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FlushAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [ + { + "Name": "disposing", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + }, + { + "Name": "bytePool", + "Type": "System.Buffers.ArrayPool<System.Byte>" + }, + { + "Name": "charPool", + "Type": "System.Buffers.ArrayPool<System.Char>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.KeyValueAccumulator", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Append", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasValues", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_KeyCount", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ValueCount", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetResults", + "Parameters": [], + "ReturnType": "System.Collections.Generic.Dictionary<System.String, Microsoft.Extensions.Primitives.StringValues>", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.MultipartReader", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HeadersCountLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HeadersCountLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HeadersLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HeadersLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_BodyLengthLimit", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BodyLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.Int64>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadNextSectionAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.WebUtilities.MultipartSection>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "boundary", + "Type": "System.String" + }, + { + "Name": "stream", + "Type": "System.IO.Stream" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "boundary", + "Type": "System.String" + }, + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "DefaultHeadersCountLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "16" + }, + { + "Kind": "Field", + "Name": "DefaultHeadersLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "16384" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.MultipartSection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ContentType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentDisposition", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "System.Collections.Generic.Dictionary<System.String, Microsoft.Extensions.Primitives.StringValues>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Headers", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.Dictionary<System.String, Microsoft.Extensions.Primitives.StringValues>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Body", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Body", + "Parameters": [ + { + "Name": "value", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_BaseStreamOffset", + "Parameters": [], + "ReturnType": "System.Nullable<System.Int64>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BaseStreamOffset", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable<System.Int64>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.MultipartSectionConverterExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AsFileSection", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + } + ], + "ReturnType": "Microsoft.AspNetCore.WebUtilities.FileMultipartSection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AsFormDataSection", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + } + ], + "ReturnType": "Microsoft.AspNetCore.WebUtilities.FormMultipartSection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetContentDispositionHeader", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.MultipartSectionStreamExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "ReadAsStringAsync", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + } + ], + "ReturnType": "System.Threading.Tasks.Task<System.String>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.QueryHelpers", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddQueryString", + "Parameters": [ + { + "Name": "uri", + "Type": "System.String" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddQueryString", + "Parameters": [ + { + "Name": "uri", + "Type": "System.String" + }, + { + "Name": "queryString", + "Type": "System.Collections.Generic.IDictionary<System.String, System.String>" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseQuery", + "Parameters": [ + { + "Name": "queryString", + "Type": "System.String" + } + ], + "ReturnType": "System.Collections.Generic.Dictionary<System.String, Microsoft.Extensions.Primitives.StringValues>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseNullableQuery", + "Parameters": [ + { + "Name": "queryString", + "Type": "System.String" + } + ], + "ReturnType": "System.Collections.Generic.Dictionary<System.String, Microsoft.Extensions.Primitives.StringValues>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.ReasonPhrases", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetReasonPhrase", + "Parameters": [ + { + "Name": "statusCode", + "Type": "System.Int32" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.StreamHelperExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "DrainAsync", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DrainAsync", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "limit", + "Type": "System.Nullable<System.Int64>" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DrainAsync", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "bytePool", + "Type": "System.Buffers.ArrayPool<System.Byte>" + }, + { + "Name": "limit", + "Type": "System.Nullable<System.Int64>" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs b/src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..a83f1574eb50a1147b691b97da7082f998bfd66d --- /dev/null +++ b/src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs @@ -0,0 +1,299 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class FileBufferingReadStreamTests + { + private Stream MakeStream(int size) + { + // TODO: Fill with random data? Make readonly? + return new MemoryStream(new byte[size]); + } + + [Fact] + public void FileBufferingReadStream_Properties_ExpectedValues() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024, null, Directory.GetCurrentDirectory())) + { + Assert.True(stream.CanRead); + Assert.True(stream.CanSeek); + Assert.False(stream.CanWrite); + Assert.Equal(0, stream.Length); // Nothing buffered yet + Assert.Equal(0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + } + } + + [Fact] + public void FileBufferingReadStream_SyncReadUnderThreshold_DoesntCreateFile() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024 * 3, null, Directory.GetCurrentDirectory())) + { + var bytes = new byte[1000]; + var read0 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read2 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(inner.Length - read0 - read1, read2); + Assert.Equal(read0 + read1 + read2, stream.Length); + Assert.Equal(read0 + read1 + read2, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read3 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(0, read3); + } + } + + [Fact] + public void FileBufferingReadStream_SyncReadOverThreshold_CreatesFile() + { + var inner = MakeStream(1024 * 2); + string tempFileName; + using (var stream = new FileBufferingReadStream(inner, 1024, null, GetCurrentDirectory())) + { + var bytes = new byte[1000]; + var read0 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + tempFileName = stream.TempFileName; + Assert.True(File.Exists(tempFileName)); + + var read2 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(inner.Length - read0 - read1, read2); + Assert.Equal(read0 + read1 + read2, stream.Length); + Assert.Equal(read0 + read1 + read2, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + Assert.True(File.Exists(tempFileName)); + + var read3 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(0, read3); + } + + Assert.False(File.Exists(tempFileName)); + } + + [Fact] + public void FileBufferingReadStream_SyncReadWithInMemoryLimit_EnforcesLimit() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024, 900, Directory.GetCurrentDirectory())) + { + var bytes = new byte[500]; + var read0 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var exception = Assert.Throws<IOException>(() => stream.Read(bytes, 0, bytes.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + Assert.False(File.Exists(stream.TempFileName)); + } + } + + [Fact] + public void FileBufferingReadStream_SyncReadWithOnDiskLimit_EnforcesLimit() + { + var inner = MakeStream(1024 * 2); + string tempFileName; + using (var stream = new FileBufferingReadStream(inner, 512, 1024, GetCurrentDirectory())) + { + var bytes = new byte[500]; + var read0 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + tempFileName = stream.TempFileName; + Assert.True(File.Exists(tempFileName)); + + var exception = Assert.Throws<IOException>(() => stream.Read(bytes, 0, bytes.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + Assert.False(File.Exists(tempFileName)); + } + + Assert.False(File.Exists(tempFileName)); + } + + /////////////////// + + [Fact] + public async Task FileBufferingReadStream_AsyncReadUnderThreshold_DoesntCreateFile() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024 * 3, null, Directory.GetCurrentDirectory())) + { + var bytes = new byte[1000]; + var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read2 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(inner.Length - read0 - read1, read2); + Assert.Equal(read0 + read1 + read2, stream.Length); + Assert.Equal(read0 + read1 + read2, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read3 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(0, read3); + } + } + + [Fact] + public async Task FileBufferingReadStream_AsyncReadOverThreshold_CreatesFile() + { + var inner = MakeStream(1024 * 2); + string tempFileName; + using (var stream = new FileBufferingReadStream(inner, 1024, null, GetCurrentDirectory())) + { + var bytes = new byte[1000]; + var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + tempFileName = stream.TempFileName; + Assert.True(File.Exists(tempFileName)); + + var read2 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(inner.Length - read0 - read1, read2); + Assert.Equal(read0 + read1 + read2, stream.Length); + Assert.Equal(read0 + read1 + read2, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + Assert.True(File.Exists(tempFileName)); + + var read3 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(0, read3); + } + + Assert.False(File.Exists(tempFileName)); + } + + [Fact] + public async Task FileBufferingReadStream_AsyncReadWithInMemoryLimit_EnforcesLimit() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024, 900, Directory.GetCurrentDirectory())) + { + var bytes = new byte[500]; + var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var exception = await Assert.ThrowsAsync<IOException>(() => stream.ReadAsync(bytes, 0, bytes.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + Assert.False(File.Exists(stream.TempFileName)); + } + } + + [Fact] + public async Task FileBufferingReadStream_AsyncReadWithOnDiskLimit_EnforcesLimit() + { + var inner = MakeStream(1024 * 2); + string tempFileName; + using (var stream = new FileBufferingReadStream(inner, 512, 1024, GetCurrentDirectory())) + { + var bytes = new byte[500]; + var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + tempFileName = stream.TempFileName; + Assert.True(File.Exists(tempFileName)); + + var exception = await Assert.ThrowsAsync<IOException>(() => stream.ReadAsync(bytes, 0, bytes.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + Assert.False(File.Exists(tempFileName)); + } + + Assert.False(File.Exists(tempFileName)); + } + + private static string GetCurrentDirectory() + { + return AppContext.BaseDirectory; + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/test/FormReaderAsyncTest.cs b/src/Http/WebUtilities/test/FormReaderAsyncTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..0a7b5e20a9c4805a870eb18e82554a459fa95c88 --- /dev/null +++ b/src/Http/WebUtilities/test/FormReaderAsyncTest.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class FormReaderAsyncTest : FormReaderTests + { + protected override async Task<Dictionary<string, StringValues>> ReadFormAsync(FormReader reader) + { + return await reader.ReadFormAsync(); + } + + protected override async Task<KeyValuePair<string, string>?> ReadPair(FormReader reader) + { + return await reader.ReadNextPairAsync(); + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/test/FormReaderTests.cs b/src/Http/WebUtilities/test/FormReaderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..134efbb51571aad094112e78da460a92672a7f08 --- /dev/null +++ b/src/Http/WebUtilities/test/FormReaderTests.cs @@ -0,0 +1,230 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class FormReaderTests + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_EmptyKeyAtEndAllowed(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "=bar"); + + var formCollection = await ReadFormAsync(new FormReader(body)); + + Assert.Equal("bar", formCollection[""].ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_EmptyKeyWithAdditionalEntryAllowed(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "=bar&baz=2"); + + var formCollection = await ReadFormAsync(new FormReader(body)); + + Assert.Equal("bar", formCollection[""].ToString()); + Assert.Equal("2", formCollection["baz"].ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_EmptyValuedAtEndAllowed(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo="); + + var formCollection = await ReadFormAsync(new FormReader(body)); + + Assert.Equal("", formCollection["foo"].ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_EmptyValuedWithAdditionalEntryAllowed(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=&baz=2"); + + var formCollection = await ReadFormAsync(new FormReader(body)); + + Assert.Equal("", formCollection["foo"].ToString()); + Assert.Equal("2", formCollection["baz"].ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitMet_Success(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&bar=2&baz=3"); + + var formCollection = await ReadFormAsync(new FormReader(body) { ValueCountLimit = 3 }); + + Assert.Equal("1", formCollection["foo"].ToString()); + Assert.Equal("2", formCollection["bar"].ToString()); + Assert.Equal("3", formCollection["baz"].ToString()); + Assert.Equal(3, formCollection.Count); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitExceeded_Throw(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&baz=2&bar=3&baz=4&baf=5"); + + var exception = await Assert.ThrowsAsync<InvalidDataException>( + () => ReadFormAsync(new FormReader(body) { ValueCountLimit = 3 })); + Assert.Equal("Form value count limit 3 exceeded.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitExceededSameKey_Throw(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "baz=1&baz=2&baz=3&baz=4"); + + var exception = await Assert.ThrowsAsync<InvalidDataException>( + () => ReadFormAsync(new FormReader(body) { ValueCountLimit = 3 })); + Assert.Equal("Form value count limit 3 exceeded.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_KeyLengthLimitMet_Success(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&bar=2&baz=3&baz=4"); + + var formCollection = await ReadFormAsync(new FormReader(body) { KeyLengthLimit = 10 }); + + Assert.Equal("1", formCollection["foo"].ToString()); + Assert.Equal("2", formCollection["bar"].ToString()); + Assert.Equal("3,4", formCollection["baz"].ToString()); + Assert.Equal(3, formCollection.Count); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_KeyLengthLimitExceeded_Throw(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&baz1234567890=2"); + + var exception = await Assert.ThrowsAsync<InvalidDataException>( + () => ReadFormAsync(new FormReader(body) { KeyLengthLimit = 10 })); + Assert.Equal("Form key or value length limit 10 exceeded.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueLengthLimitMet_Success(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&bar=1234567890&baz=3&baz=4"); + + var formCollection = await ReadFormAsync(new FormReader(body) { ValueLengthLimit = 10 }); + + Assert.Equal("1", formCollection["foo"].ToString()); + Assert.Equal("1234567890", formCollection["bar"].ToString()); + Assert.Equal("3,4", formCollection["baz"].ToString()); + Assert.Equal(3, formCollection.Count); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueLengthLimitExceeded_Throw(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&baz=1234567890123"); + + var exception = await Assert.ThrowsAsync<InvalidDataException>( + () => ReadFormAsync(new FormReader(body) { ValueLengthLimit = 10 })); + Assert.Equal("Form key or value length limit 10 exceeded.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadNextPair_ReadsAllPairs(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=&baz=2"); + + var reader = new FormReader(body); + + var pair = (KeyValuePair<string, string>)await ReadPair(reader); + + Assert.Equal("foo", pair.Key); + Assert.Equal("", pair.Value); + + pair = (KeyValuePair<string, string>)await ReadPair(reader); + + Assert.Equal("baz", pair.Key); + Assert.Equal("2", pair.Value); + + Assert.Null(await ReadPair(reader)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadNextPair_ReturnsNullOnEmptyStream(bool bufferRequest) + { + var body = MakeStream(bufferRequest, ""); + + var reader = new FormReader(body); + + Assert.Null(await ReadPair(reader)); + } + + // https://en.wikipedia.org/wiki/Percent-encoding + [Theory] + [InlineData("++=hello", " ", "hello")] + [InlineData("a=1+1", "a", "1 1")] + [InlineData("%22%25%2D%2E%3C%3E%5C%5E%5F%60%7B%7C%7D%7E=%22%25%2D%2E%3C%3E%5C%5E%5F%60%7B%7C%7D%7E", "\"%-.<>\\^_`{|}~", "\"%-.<>\\^_`{|}~")] + [InlineData("a=%41", "a", "A")] // ascii encoded hex + [InlineData("a=%C3%A1", "a", "\u00e1")] // utf8 code points + [InlineData("a=%u20AC", "a", "%u20AC")] // utf16 not supported + public async Task ReadForm_Decoding(string formData, string key, string expectedValue) + { + var body = MakeStream(bufferRequest: false, text: formData); + + var form = await ReadFormAsync(new FormReader(body)); + + Assert.Equal(expectedValue, form[key]); + } + + protected virtual Task<Dictionary<string, StringValues>> ReadFormAsync(FormReader reader) + { + return Task.FromResult(reader.ReadForm()); + } + + protected virtual Task<KeyValuePair<string, string>?> ReadPair(FormReader reader) + { + return Task.FromResult(reader.ReadNextPair()); + } + + private static Stream MakeStream(bool bufferRequest, string text) + { + var formContent = Encoding.UTF8.GetBytes(text); + Stream body = new MemoryStream(formContent); + if (!bufferRequest) + { + body = new NonSeekableReadStream(body); + } + return body; + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs b/src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..062342fa4cda7312e52ab2c826672577c6063f94 --- /dev/null +++ b/src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs @@ -0,0 +1,313 @@ +// 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 Moq; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + + +namespace Microsoft.AspNetCore.WebUtilities.Test +{ + public class HttpResponseStreamReaderTest + { + private static readonly char[] CharData = new char[] + { + char.MinValue, + char.MaxValue, + '\t', + ' ', + '$', + '@', + '#', + '\0', + '\v', + '\'', + '\u3190', + '\uC3A0', + 'A', + '5', + '\r', + '\uFE70', + '-', + ';', + '\r', + '\n', + 'T', + '3', + '\n', + 'K', + '\u00E6', + }; + + [Fact] + public static async Task ReadToEndAsync() + { + // Arrange + var reader = new HttpRequestStreamReader(GetLargeStream(), Encoding.UTF8); + + var result = await reader.ReadToEndAsync(); + + Assert.Equal(5000, result.Length); + } + + [Fact] + public static void TestRead() + { + // Arrange + var reader = CreateReader(); + + // Act & Assert + for (var i = 0; i < CharData.Length; i++) + { + var tmp = reader.Read(); + Assert.Equal((int)CharData[i], tmp); + } + } + + [Fact] + public static void TestPeek() + { + // Arrange + var reader = CreateReader(); + + // Act & Assert + for (var i = 0; i < CharData.Length; i++) + { + var peek = reader.Peek(); + Assert.Equal((int)CharData[i], peek); + + reader.Read(); + } + } + + [Fact] + public static void EmptyStream() + { + // Arrange + var reader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8); + var buffer = new char[10]; + + // Act + var read = reader.Read(buffer, 0, 1); + + // Assert + Assert.Equal(0, read); + } + + [Fact] + public static void Read_ReadAllCharactersAtOnce() + { + // Arrange + var reader = CreateReader(); + var chars = new char[CharData.Length]; + + // Act + var read = reader.Read(chars, 0, chars.Length); + + // Assert + Assert.Equal(chars.Length, read); + for (var i = 0; i < CharData.Length; i++) + { + Assert.Equal(CharData[i], chars[i]); + } + } + + [Fact] + public static async Task Read_ReadInTwoChunks() + { + // Arrange + var reader = CreateReader(); + var chars = new char[CharData.Length]; + + // Act + var read = await reader.ReadAsync(chars, 4, 3); + + // Assert + Assert.Equal(3, read); + for (var i = 0; i < 3; i++) + { + Assert.Equal(CharData[i], chars[i + 4]); + } + } + + [Fact] + public static void ReadLine_ReadMultipleLines() + { + // Arrange + var reader = CreateReader(); + var valueString = new string(CharData); + + // Act & Assert + var data = reader.ReadLine(); + Assert.Equal(valueString.Substring(0, valueString.IndexOf('\r')), data); + + data = reader.ReadLine(); + Assert.Equal(valueString.Substring(valueString.IndexOf('\r') + 1, 3), data); + + data = reader.ReadLine(); + Assert.Equal(valueString.Substring(valueString.IndexOf('\n') + 1, 2), data); + + data = reader.ReadLine(); + Assert.Equal((valueString.Substring(valueString.LastIndexOf('\n') + 1)), data); + } + + [Fact] + public static void ReadLine_ReadWithNoNewlines() + { + // Arrange + var reader = CreateReader(); + var valueString = new string(CharData); + var temp = new char[10]; + + // Act + reader.Read(temp, 0, 1); + var data = reader.ReadLine(); + + // Assert + Assert.Equal(valueString.Substring(1, valueString.IndexOf('\r') - 1), data); + } + + [Fact] + public static async Task ReadLineAsync_MultipleContinuousLines() + { + // Arrange + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write("\n\n\r\r\n"); + writer.Flush(); + stream.Position = 0; + + var reader = new HttpRequestStreamReader(stream, Encoding.UTF8); + + // Act & Assert + for (var i = 0; i < 4; i++) + { + var data = await reader.ReadLineAsync(); + Assert.Equal(string.Empty, data); + } + + var eol = await reader.ReadLineAsync(); + Assert.Null(eol); + } + + [Theory] + [MemberData(nameof(HttpRequestNullData))] + public static void NullInputsInConstructor_ExpectArgumentNullException(Stream stream, Encoding encoding, ArrayPool<byte> bytePool, ArrayPool<char> charPool) + { + Assert.Throws<ArgumentNullException>(() => + { + var httpRequestStreamReader = new HttpRequestStreamReader(stream, encoding, 1, bytePool, charPool); + }); + } + + + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public static void NegativeOrZeroBufferSize_ExpectArgumentOutOfRangeException(int size) + { + Assert.Throws<ArgumentOutOfRangeException>(() => + { + var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, size, ArrayPool<byte>.Shared, ArrayPool<char>.Shared); + }); + } + + [Fact] + public static void StreamCannotRead_ExpectArgumentException() + { + var mockStream = new Mock<Stream>(); + mockStream.Setup(m => m.CanRead).Returns(false); + Assert.Throws<ArgumentException>(() => + { + var httpRequestStreamReader = new HttpRequestStreamReader(mockStream.Object, Encoding.UTF8, 1, ArrayPool<byte>.Shared, ArrayPool<char>.Shared); + }); + } + + [Theory] + [MemberData(nameof(HttpRequestDisposeData))] + public static void StreamDisposed_ExpectedObjectDisposedException(Action<HttpRequestStreamReader> action) + { + var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, 10, ArrayPool<byte>.Shared, ArrayPool<char>.Shared); + httpRequestStreamReader.Dispose(); + + Assert.Throws<ObjectDisposedException>(() => + { + action(httpRequestStreamReader); + }); + } + + [Fact] + public static async Task StreamDisposed_ExpectObjectDisposedExceptionAsync() + { + var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, 10, ArrayPool<byte>.Shared, ArrayPool<char>.Shared); + httpRequestStreamReader.Dispose(); + + await Assert.ThrowsAsync<ObjectDisposedException>(() => + { + return httpRequestStreamReader.ReadAsync(new char[10], 0, 1); + }); + } + private static HttpRequestStreamReader CreateReader() + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(CharData); + writer.Flush(); + stream.Position = 0; + + return new HttpRequestStreamReader(stream, Encoding.UTF8); + } + + private static MemoryStream GetSmallStream() + { + var testData = new byte[] { 72, 69, 76, 76, 79 }; + return new MemoryStream(testData); + } + + private static MemoryStream GetLargeStream() + { + var testData = new byte[] { 72, 69, 76, 76, 79 }; + // System.Collections.Generic. + + var data = new List<byte>(); + for (var i = 0; i < 1000; i++) + { + data.AddRange(testData); + } + + return new MemoryStream(data.ToArray()); + } + + public static IEnumerable<object[]> HttpRequestNullData() + { + yield return new object[] { null, Encoding.UTF8, ArrayPool<byte>.Shared, ArrayPool<char>.Shared }; + yield return new object[] { new MemoryStream(), null, ArrayPool<byte>.Shared, ArrayPool<char>.Shared }; + yield return new object[] { new MemoryStream(), Encoding.UTF8, null, ArrayPool<char>.Shared }; + yield return new object[] { new MemoryStream(), Encoding.UTF8, ArrayPool<byte>.Shared, null }; + } + + public static IEnumerable<object[]> HttpRequestDisposeData() + { + yield return new object[] { new Action<HttpRequestStreamReader>((httpRequestStreamReader) => + { + var res = httpRequestStreamReader.Read(); + })}; + yield return new object[] { new Action<HttpRequestStreamReader>((httpRequestStreamReader) => + { + var res = httpRequestStreamReader.Read(new char[10], 0, 1); + })}; + + yield return new object[] { new Action<HttpRequestStreamReader>((httpRequestStreamReader) => + { + var res = httpRequestStreamReader.Peek(); + })}; + + } + } +} diff --git a/src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs b/src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..7847e1384e24a8620e0bbaa1bb01ec18e5ef69d8 --- /dev/null +++ b/src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs @@ -0,0 +1,574 @@ +// 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 Moq; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.WebUtilities.Test +{ + public class HttpResponseStreamWriterTest + { + private const int DefaultCharacterChunkSize = HttpResponseStreamWriter.DefaultBufferSize; + + [Fact] + public async Task DoesNotWriteBOM() + { + // Arrange + var memoryStream = new MemoryStream(); + var encodingWithBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true); + var writer = new HttpResponseStreamWriter(memoryStream, encodingWithBOM); + var expectedData = new byte[] { 97, 98, 99, 100 }; // without BOM + + // Act + using (writer) + { + await writer.WriteAsync("abcd"); + } + + // Assert + Assert.Equal(expectedData, memoryStream.ToArray()); + } + + [Fact] + public async Task DoesNotFlush_UnderlyingStream_OnDisposingWriter() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + await writer.WriteAsync("Hello"); + writer.Dispose(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + } + + [Fact] + public async Task DoesNotDispose_UnderlyingStream_OnDisposingWriter() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + await writer.WriteAsync("Hello world"); + writer.Dispose(); + + // Assert + Assert.Equal(0, stream.DisposeCallCount); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task FlushesBuffer_OnClose(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + await writer.WriteAsync(new string('a', byteLength)); + + // Act + writer.Dispose(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task FlushesBuffer_OnDispose(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + await writer.WriteAsync(new string('a', byteLength)); + + // Act + writer.Dispose(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(byteLength, stream.Length); + } + + [Fact] + public void NoDataWritten_Flush_DoesNotFlushUnderlyingStream() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + writer.Flush(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public void FlushesBuffer_ButNotStream_OnFlush(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + writer.Write(new string('a', byteLength)); + + var expectedWriteCount = Math.Ceiling((double)byteLength / HttpResponseStreamWriter.DefaultBufferSize); + + // Act + writer.Flush(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(expectedWriteCount, stream.WriteCallCount); + Assert.Equal(byteLength, stream.Length); + } + + [Fact] + public async Task NoDataWritten_FlushAsync_DoesNotFlushUnderlyingStream() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + await writer.FlushAsync(); + + // Assert + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(0, stream.Length); + } + + [Theory] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize - 1)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize * 2)] + public async Task FlushesBuffer_ButNotStream_OnFlushAsync(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + await writer.WriteAsync(new string('a', byteLength)); + + var expectedWriteCount = Math.Ceiling((double)byteLength / HttpResponseStreamWriter.DefaultBufferSize); + + // Act + await writer.FlushAsync(); + + // Assert + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(expectedWriteCount, stream.WriteAsyncCallCount); + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + public async Task FlushWriteThrows_DontFlushInDispose(int byteLength) + { + // Arrange + var stream = new TestMemoryStream() { ThrowOnWrite = true }; + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + await writer.WriteAsync(new string('a', byteLength)); + await Assert.ThrowsAsync<IOException>(() => writer.FlushAsync()); + + // Act + writer.Dispose(); + + // Assert + Assert.Equal(1, stream.WriteAsyncCallCount); + Assert.Equal(0, stream.WriteCallCount); + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(0, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public void WriteChar_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + for (var i = 0; i < byteLength; i++) + { + writer.Write('a'); + } + } + + // Assert + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public void WriteCharArray_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + writer.Write((new string('a', byteLength)).ToCharArray()); + } + + // Assert + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task WriteCharAsync_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + for (var i = 0; i < byteLength; i++) + { + await writer.WriteAsync('a'); + } + } + + // Assert + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task WriteCharArrayAsync_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + await writer.WriteAsync((new string('a', byteLength)).ToCharArray()); + } + + // Assert + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData("ä½ å¥½ä¸–ç•Œ", "utf-16")] + [InlineData("హలో à°ªà±à°°à°ªà°‚à°š", "iso-8859-1")] + [InlineData("வணகà¯à®•à®®à¯ உலக", "utf-32")] + public async Task WritesData_InExpectedEncoding(string data, string encodingName) + { + // Arrange + var encoding = Encoding.GetEncoding(encodingName); + var expectedBytes = encoding.GetBytes(data); + var stream = new MemoryStream(); + var writer = new HttpResponseStreamWriter(stream, encoding); + + // Act + using (writer) + { + await writer.WriteAsync(data); + } + + // Assert + Assert.Equal(expectedBytes, stream.ToArray()); + } + + [Theory] + [InlineData('ã‚“', 1023, "utf-8")] + [InlineData('ã‚“', 1024, "utf-8")] + [InlineData('ã‚“', 1050, "utf-8")] + [InlineData('ä½ ', 1023, "utf-16")] + [InlineData('ä½ ', 1024, "utf-16")] + [InlineData('ä½ ', 1050, "utf-16")] + [InlineData('à°¹', 1023, "iso-8859-1")] + [InlineData('à°¹', 1024, "iso-8859-1")] + [InlineData('à°¹', 1050, "iso-8859-1")] + [InlineData('வ', 1023, "utf-32")] + [InlineData('வ', 1024, "utf-32")] + [InlineData('வ', 1050, "utf-32")] + public async Task WritesData_OfDifferentLength_InExpectedEncoding( + char character, + int charCount, + string encodingName) + { + // Arrange + var encoding = Encoding.GetEncoding(encodingName); + string data = new string(character, charCount); + var expectedBytes = encoding.GetBytes(data); + var stream = new MemoryStream(); + var writer = new HttpResponseStreamWriter(stream, encoding); + + // Act + using (writer) + { + await writer.WriteAsync(data); + } + + // Assert + Assert.Equal(expectedBytes, stream.ToArray()); + } + + // None of the code in HttpResponseStreamWriter differs significantly when using pooled buffers. + // + // This test effectively verifies that things are correctly constructed and disposed. Pooled buffers + // throw on the finalizer thread if not disposed, so that's why it's complicated. + [Fact] + public void HttpResponseStreamWriter_UsingPooledBuffers() + { + // Arrange + var encoding = Encoding.UTF8; + var stream = new MemoryStream(); + + var expectedBytes = encoding.GetBytes("Hello, World!"); + + using (var writer = new HttpResponseStreamWriter( + stream, + encoding, + 1024, + ArrayPool<byte>.Shared, + ArrayPool<char>.Shared)) + { + // Act + writer.Write("Hello, World!"); + } + + // Assert + Assert.Equal(expectedBytes, stream.ToArray()); + } + + [Theory] + [InlineData(DefaultCharacterChunkSize)] + [InlineData(DefaultCharacterChunkSize * 2)] + [InlineData(DefaultCharacterChunkSize * 3)] + public async Task HttpResponseStreamWriter_WritesDataCorrectly_ForCharactersHavingSurrogatePairs(int characterSize) + { + // Arrange + // Here "ð€" (called Deseret Long I) actually represents 2 characters. Try to make this character split across + // the boundary + var content = new string('a', characterSize - 1) + "ð€"; + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.Unicode); + + // Act + await writer.WriteAsync(content); + await writer.FlushAsync(); + + // Assert + stream.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(stream, Encoding.Unicode); + var actualContent = await streamReader.ReadToEndAsync(); + Assert.Equal(content, actualContent); + } + + [Theory] + [MemberData(nameof(HttpResponseStreamWriterData))] + public static void NullInputsInConstructor_ExpectArgumentNullException(Stream stream, Encoding encoding, ArrayPool<byte> bytePool, ArrayPool<char> charPool) + { + Assert.Throws<ArgumentNullException>(() => + { + var httpRequestStreamReader = new HttpResponseStreamWriter(stream, encoding, 1, bytePool, charPool); + }); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public static void NegativeOrZeroBufferSize_ExpectArgumentOutOfRangeException(int size) + { + Assert.Throws<ArgumentOutOfRangeException>(() => + { + var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, size, ArrayPool<byte>.Shared, ArrayPool<char>.Shared); + }); + } + + [Fact] + public static void StreamCannotRead_ExpectArgumentException() + { + var mockStream = new Mock<Stream>(); + mockStream.Setup(m => m.CanWrite).Returns(false); + Assert.Throws<ArgumentException>(() => + { + var httpRequestStreamReader = new HttpRequestStreamReader(mockStream.Object, Encoding.UTF8, 1, ArrayPool<byte>.Shared, ArrayPool<char>.Shared); + }); + } + + [Theory] + [MemberData(nameof(HttpResponseDisposeData))] + public static void StreamDisposed_ExpectedObjectDisposedException(Action<HttpResponseStreamWriter> action) + { + var httpResponseStreamWriter = new HttpResponseStreamWriter(new MemoryStream(), Encoding.UTF8, 10, ArrayPool<byte>.Shared, ArrayPool<char>.Shared); + httpResponseStreamWriter.Dispose(); + + Assert.Throws<ObjectDisposedException>(() => + { + action(httpResponseStreamWriter); + }); + } + + [Theory] + [MemberData(nameof(HttpResponseDisposeDataAsync))] + public static async Task StreamDisposed_ExpectedObjectDisposedExceptionAsync(Func<HttpResponseStreamWriter, Task> function) + { + var httpResponseStreamWriter = new HttpResponseStreamWriter(new MemoryStream(), Encoding.UTF8, 10, ArrayPool<byte>.Shared, ArrayPool<char>.Shared); + httpResponseStreamWriter.Dispose(); + + await Assert.ThrowsAsync<ObjectDisposedException>(() => + { + return function(httpResponseStreamWriter); + }); + } + + + private class TestMemoryStream : MemoryStream + { + public int FlushCallCount { get; private set; } + + public int FlushAsyncCallCount { get; private set; } + + public int CloseCallCount { get; private set; } + + public int DisposeCallCount { get; private set; } + + public int WriteCallCount { get; private set; } + + public int WriteAsyncCallCount { get; private set; } + + public bool ThrowOnWrite { get; set; } + + public override void Flush() + { + FlushCallCount++; + base.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + FlushAsyncCallCount++; + return base.FlushAsync(cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) + { + WriteCallCount++; + if (ThrowOnWrite) + { + throw new IOException("Test IOException"); + } + base.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + WriteAsyncCallCount++; + if (ThrowOnWrite) + { + throw new IOException("Test IOException"); + } + return base.WriteAsync(buffer, offset, count, cancellationToken); + } + + protected override void Dispose(bool disposing) + { + DisposeCallCount++; + base.Dispose(disposing); + } + } + + public static IEnumerable<object[]> HttpResponseStreamWriterData() + { + yield return new object[] { null, Encoding.UTF8, ArrayPool<byte>.Shared, ArrayPool<char>.Shared }; + yield return new object[] { new MemoryStream(), null, ArrayPool<byte>.Shared, ArrayPool<char>.Shared }; + yield return new object[] { new MemoryStream(), Encoding.UTF8, null, ArrayPool<char>.Shared }; + yield return new object[] { new MemoryStream(), Encoding.UTF8, ArrayPool<byte>.Shared, null }; + } + + public static IEnumerable<object[]> HttpResponseDisposeData() + { + yield return new object[] { new Action<HttpResponseStreamWriter>((httpResponseStreamWriter) => + { + httpResponseStreamWriter.Write('a'); + })}; + yield return new object[] { new Action<HttpResponseStreamWriter>((httpResponseStreamWriter) => + { + httpResponseStreamWriter.Write(new char[] { 'a', 'b' }, 0, 1); + })}; + + yield return new object[] { new Action<HttpResponseStreamWriter>((httpResponseStreamWriter) => + { + httpResponseStreamWriter.Write("hello"); + })}; + yield return new object[] { new Action<HttpResponseStreamWriter>((httpResponseStreamWriter) => + { + httpResponseStreamWriter.Flush(); + })}; + } + + public static IEnumerable<object[]> HttpResponseDisposeDataAsync() + { + yield return new object[] { new Func<HttpResponseStreamWriter, Task>(async (httpResponseStreamWriter) => + { + await httpResponseStreamWriter.WriteAsync('a'); + })}; + yield return new object[] { new Func<HttpResponseStreamWriter, Task>(async (httpResponseStreamWriter) => + { + await httpResponseStreamWriter.WriteAsync(new char[] { 'a', 'b' }, 0, 1); + })}; + + yield return new object[] { new Func<HttpResponseStreamWriter, Task>(async (httpResponseStreamWriter) => + { + await httpResponseStreamWriter.WriteAsync("hello"); + })}; + yield return new object[] { new Func<HttpResponseStreamWriter, Task>(async (httpResponseStreamWriter) => + { + await httpResponseStreamWriter.FlushAsync(); + })}; + } + } +} diff --git a/src/Http/WebUtilities/test/Microsoft.AspNetCore.WebUtilities.Tests.csproj b/src/Http/WebUtilities/test/Microsoft.AspNetCore.WebUtilities.Tests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..8a91421e65d405abd8c91914ca30d62c2307dba3 --- /dev/null +++ b/src/Http/WebUtilities/test/Microsoft.AspNetCore.WebUtilities.Tests.csproj @@ -0,0 +1,11 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.WebUtilities" /> + </ItemGroup> + +</Project> diff --git a/src/Http/WebUtilities/test/MultipartReaderTests.cs b/src/Http/WebUtilities/test/MultipartReaderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..d66ea98fedfaaf05f7ae38f8f914d219c9ab0234 --- /dev/null +++ b/src/Http/WebUtilities/test/MultipartReaderTests.cs @@ -0,0 +1,383 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class MultipartReaderTests + { + private const string Boundary = "9051914041544843365972754266"; + // Note that CRLF (\r\n) is required. You can't use multi-line C# strings here because the line breaks on Linux are just LF. + private const string OnePartBody = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + private const string OnePartBodyTwoHeaders = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"Custom-header: custom-value\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + private const string OnePartBodyWithTrailingWhitespace = +"--9051914041544843365972754266 \r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + // It's non-compliant but common to leave off the last CRLF. + private const string OnePartBodyWithoutFinalCRLF = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--"; + private const string TwoPartBody = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\r\n" + +"Content-Type: text/plain\r\n" + +"\r\n" + +"Content of a.txt.\r\n" + +"\r\n" + +"--9051914041544843365972754266--\r\n"; + private const string TwoPartBodyWithUnicodeFileName = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file1\"; filename=\"a色.txt\"\r\n" + +"Content-Type: text/plain\r\n" + +"\r\n" + +"Content of a.txt.\r\n" + +"\r\n" + +"--9051914041544843365972754266--\r\n"; + private const string ThreePartBody = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\r\n" + +"Content-Type: text/plain\r\n" + +"\r\n" + +"Content of a.txt.\r\n" + +"\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file2\"; filename=\"a.html\"\r\n" + +"Content-Type: text/html\r\n" + +"\r\n" + +"<!DOCTYPE html><title>Content of a.html.</title>\r\n" + +"\r\n" + +"--9051914041544843365972754266--\r\n"; + + private const string TwoPartBodyIncompleteBuffer = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\r\n" + +"Content-Type: text/plain\r\n" + +"\r\n" + +"Content of a.txt.\r\n" + +"\r\n" + +"--9051914041544843365"; + + private static MemoryStream MakeStream(string text) + { + return new MemoryStream(Encoding.UTF8.GetBytes(text)); + } + + private static string GetString(byte[] buffer, int count) + { + return Encoding.ASCII.GetString(buffer, 0, count); + } + + [Fact] + public async Task MutipartReader_ReadSinglePartBody_Success() + { + var stream = MakeStream(OnePartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_HeaderCountExceeded_Throws() + { + var stream = MakeStream(OnePartBodyTwoHeaders); + var reader = new MultipartReader(Boundary, stream) + { + HeadersCountLimit = 1, + }; + + var exception = await Assert.ThrowsAsync<InvalidDataException>(() => reader.ReadNextSectionAsync()); + Assert.Equal("Multipart headers count limit 1 exceeded.", exception.Message); + } + + [Fact] + public async Task MutipartReader_HeadersLengthExceeded_Throws() + { + var stream = MakeStream(OnePartBodyTwoHeaders); + var reader = new MultipartReader(Boundary, stream) + { + HeadersLengthLimit = 60, + }; + + var exception = await Assert.ThrowsAsync<InvalidDataException>(() => reader.ReadNextSectionAsync()); + Assert.Equal("Line length limit 17 exceeded.", exception.Message); + } + + [Fact] + public async Task MutipartReader_ReadSinglePartBodyWithTrailingWhitespace_Success() + { + var stream = MakeStream(OnePartBodyWithTrailingWhitespace); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ReadSinglePartBodyWithoutLastCRLF_Success() + { + var stream = MakeStream(OnePartBodyWithoutFinalCRLF); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ReadTwoPartBody_Success() + { + var stream = MakeStream(TwoPartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ReadTwoPartBodyWithUnicodeFileName_Success() + { + var stream = MakeStream(TwoPartBodyWithUnicodeFileName); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a色.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ThreePartBody_Success() + { + var stream = MakeStream(ThreePartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file2\"; filename=\"a.html\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/html", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("<!DOCTYPE html><title>Content of a.html.</title>\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public void MutipartReader_BufferSizeMustBeLargerThanBoundary_Throws() + { + var stream = MakeStream(ThreePartBody); + Assert.Throws<ArgumentOutOfRangeException>(() => + { + var reader = new MultipartReader(Boundary, stream, 5); + }); + } + + [Fact] + public async Task MutipartReader_TwoPartBodyIncompleteBuffer_TwoSectionsReadSuccessfullyThirdSectionThrows() + { + var stream = MakeStream(TwoPartBodyIncompleteBuffer); + var reader = new MultipartReader(Boundary, stream); + var buffer = new byte[128]; + + //first section can be read successfully + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var read = section.Body.Read(buffer, 0, buffer.Length); + Assert.Equal("text default", GetString(buffer, read)); + + //second section can be read successfully (even though the bottom boundary is truncated) + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + read = section.Body.Read(buffer, 0, buffer.Length); + Assert.Equal("Content of a.txt.\r\n", GetString(buffer, read)); + + await Assert.ThrowsAsync<IOException>(async () => + { + // we'll be unable to ensure enough bytes are buffered to even contain a final boundary + section = await reader.ReadNextSectionAsync(); + }); + } + + [Fact] + public async Task MutipartReader_ReadInvalidUtf8Header_ReplacementCharacters() + { + var body1 = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\" filename=\"a"; + + var body2 = +".txt\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + var stream = new MemoryStream(); + var bytes = Encoding.UTF8.GetBytes(body1); + stream.Write(bytes, 0, bytes.Length); + + // Write an invalid utf-8 segment in the middle + stream.Write(new byte[] { 0xC1, 0x21 }, 0, 2); + + bytes = Encoding.UTF8.GetBytes(body2); + stream.Write(bytes, 0, bytes.Length); + stream.Seek(0, SeekOrigin.Begin); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\" filename=\"a\uFFFD!.txt\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ReadInvalidUtf8SurrogateHeader_ReplacementCharacters() + { + var body1 = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\" filename=\"a"; + + var body2 = +".txt\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + var stream = new MemoryStream(); + var bytes = Encoding.UTF8.GetBytes(body1); + stream.Write(bytes, 0, bytes.Length); + + // Write an invalid utf-8 segment in the middle + stream.Write(new byte[] { 0xED, 0xA0, 85 }, 0, 3); + + bytes = Encoding.UTF8.GetBytes(body2); + stream.Write(bytes, 0, bytes.Length); + stream.Seek(0, SeekOrigin.Begin); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\" filename=\"a\uFFFDU.txt\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/test/NonSeekableReadStream.cs b/src/Http/WebUtilities/test/NonSeekableReadStream.cs new file mode 100644 index 0000000000000000000000000000000000000000..f3c77abb38529d82bda6796b6cb0e98ebfb1c81c --- /dev/null +++ b/src/Http/WebUtilities/test/NonSeekableReadStream.cs @@ -0,0 +1,74 @@ +// 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.WebUtilities +{ + public class NonSeekableReadStream : Stream + { + private Stream _inner; + + public NonSeekableReadStream(byte[] data) + : this(new MemoryStream(data)) + { + } + + public NonSeekableReadStream(Stream inner) + { + _inner = inner; + } + + public override bool CanRead => _inner.CanRead; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + 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) + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + count = Math.Max(count, 1); + return _inner.Read(buffer, offset, count); + } + + public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + count = Math.Max(count, 1); + return _inner.ReadAsync(buffer, offset, count, cancellationToken); + } + } +} diff --git a/src/Http/WebUtilities/test/QueryHelpersTests.cs b/src/Http/WebUtilities/test/QueryHelpersTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..5607ab87aa24014c3dad03c500c45f129fab74ef --- /dev/null +++ b/src/Http/WebUtilities/test/QueryHelpersTests.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class QueryHelperTests + { + [Fact] + public void ParseQueryWithUniqueKeysWorks() + { + var collection = QueryHelpers.ParseQuery("?key1=value1&key2=value2"); + Assert.Equal(2, collection.Count); + Assert.Equal("value1", collection["key1"].FirstOrDefault()); + Assert.Equal("value2", collection["key2"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithoutQuestionmarkWorks() + { + var collection = QueryHelpers.ParseQuery("key1=value1&key2=value2"); + Assert.Equal(2, collection.Count); + Assert.Equal("value1", collection["key1"].FirstOrDefault()); + Assert.Equal("value2", collection["key2"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithDuplicateKeysGroups() + { + var collection = QueryHelpers.ParseQuery("?key1=valueA&key2=valueB&key1=valueC"); + Assert.Equal(2, collection.Count); + Assert.Equal(new[] { "valueA", "valueC" }, collection["key1"]); + Assert.Equal("valueB", collection["key2"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithEmptyValuesWorks() + { + var collection = QueryHelpers.ParseQuery("?key1=&key2="); + Assert.Equal(2, collection.Count); + Assert.Equal(string.Empty, collection["key1"].FirstOrDefault()); + Assert.Equal(string.Empty, collection["key2"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithEmptyKeyWorks() + { + var collection = QueryHelpers.ParseQuery("?=value1&="); + Assert.Single(collection); + Assert.Equal(new[] { "value1", "" }, collection[""]); + } + + [Theory] + [InlineData("http://contoso.com/", "http://contoso.com/?hello=world")] + [InlineData("http://contoso.com/someaction", "http://contoso.com/someaction?hello=world")] + [InlineData("http://contoso.com/someaction?q=test", "http://contoso.com/someaction?q=test&hello=world")] + [InlineData( + "http://contoso.com/someaction?q=test#anchor", + "http://contoso.com/someaction?q=test&hello=world#anchor")] + [InlineData("http://contoso.com/someaction#anchor", "http://contoso.com/someaction?hello=world#anchor")] + [InlineData("http://contoso.com/#anchor", "http://contoso.com/?hello=world#anchor")] + [InlineData( + "http://contoso.com/someaction?q=test#anchor?value", + "http://contoso.com/someaction?q=test&hello=world#anchor?value")] + [InlineData( + "http://contoso.com/someaction#anchor?stuff", + "http://contoso.com/someaction?hello=world#anchor?stuff")] + [InlineData( + "http://contoso.com/someaction?name?something", + "http://contoso.com/someaction?name?something&hello=world")] + [InlineData( + "http://contoso.com/someaction#name#something", + "http://contoso.com/someaction?hello=world#name#something")] + public void AddQueryStringWithKeyAndValue(string uri, string expectedUri) + { + var result = QueryHelpers.AddQueryString(uri, "hello", "world"); + Assert.Equal(expectedUri, result); + } + + [Theory] + [InlineData("http://contoso.com/", "http://contoso.com/?hello=world&some=text")] + [InlineData("http://contoso.com/someaction", "http://contoso.com/someaction?hello=world&some=text")] + [InlineData("http://contoso.com/someaction?q=1", "http://contoso.com/someaction?q=1&hello=world&some=text")] + [InlineData("http://contoso.com/some#action", "http://contoso.com/some?hello=world&some=text#action")] + [InlineData("http://contoso.com/some?q=1#action", "http://contoso.com/some?q=1&hello=world&some=text#action")] + [InlineData("http://contoso.com/#action", "http://contoso.com/?hello=world&some=text#action")] + [InlineData( + "http://contoso.com/someaction?q=test#anchor?value", + "http://contoso.com/someaction?q=test&hello=world&some=text#anchor?value")] + [InlineData( + "http://contoso.com/someaction#anchor?stuff", + "http://contoso.com/someaction?hello=world&some=text#anchor?stuff")] + [InlineData( + "http://contoso.com/someaction?name?something", + "http://contoso.com/someaction?name?something&hello=world&some=text")] + [InlineData( + "http://contoso.com/someaction#name#something", + "http://contoso.com/someaction?hello=world&some=text#name#something")] + public void AddQueryStringWithDictionary(string uri, string expectedUri) + { + var queryStrings = new Dictionary<string, string>() + { + { "hello", "world" }, + { "some", "text" } + }; + + var result = QueryHelpers.AddQueryString(uri, queryStrings); + Assert.Equal(expectedUri, result); + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/test/WebEncodersTests.cs b/src/Http/WebUtilities/test/WebEncodersTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..bb7f71248f58c68809c3a30e21a7a64c5f8202bd --- /dev/null +++ b/src/Http/WebUtilities/test/WebEncodersTests.cs @@ -0,0 +1,65 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class WebEncodersTests + { + + [Theory] + [InlineData("", 1, 0)] + [InlineData("", 0, 1)] + [InlineData("0123456789", 9, 2)] + [InlineData("0123456789", Int32.MaxValue, 2)] + [InlineData("0123456789", 9, -1)] + public void Base64UrlDecode_BadOffsets(string input, int offset, int count) + { + // Act & assert + Assert.ThrowsAny<ArgumentException>(() => + { + var retVal = WebEncoders.Base64UrlDecode(input, offset, count); + }); + } + + [Theory] + [InlineData(0, 1, 0)] + [InlineData(0, 0, 1)] + [InlineData(10, 9, 2)] + [InlineData(10, Int32.MaxValue, 2)] + [InlineData(10, 9, -1)] + public void Base64UrlEncode_BadOffsets(int inputLength, int offset, int count) + { + // Arrange + byte[] input = new byte[inputLength]; + + // Act & assert + Assert.ThrowsAny<ArgumentException>(() => + { + var retVal = WebEncoders.Base64UrlEncode(input, offset, count); + }); + } + + [Fact] + public void DataOfVariousLengthRoundTripCorrectly() + { + for (int length = 0; length != 256; ++length) + { + var data = new byte[length]; + for (int index = 0; index != length; ++index) + { + data[index] = (byte)(5 + length + (index * 23)); + } + string text = WebEncoders.Base64UrlEncode(data); + byte[] result = WebEncoders.Base64UrlDecode(text); + + for (int index = 0; index != length; ++index) + { + Assert.Equal(data[index], result[index]); + } + } + } + } +} diff --git a/src/Http/samples/SampleApp/PooledHttpContext.cs b/src/Http/samples/SampleApp/PooledHttpContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..58166bb572aef3da4592bee3b0dda7ec956973a9 --- /dev/null +++ b/src/Http/samples/SampleApp/PooledHttpContext.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Internal; + +namespace SampleApp +{ + public class PooledHttpContext : DefaultHttpContext + { + DefaultHttpRequest _pooledHttpRequest; + DefaultHttpResponse _pooledHttpResponse; + + public PooledHttpContext(IFeatureCollection featureCollection) : + base(featureCollection) + { + } + + protected override HttpRequest InitializeHttpRequest() + { + if (_pooledHttpRequest != null) + { + _pooledHttpRequest.Initialize(this); + return _pooledHttpRequest; + } + + return new DefaultHttpRequest(this); + } + + protected override void UninitializeHttpRequest(HttpRequest instance) + { + _pooledHttpRequest = instance as DefaultHttpRequest; + _pooledHttpRequest?.Uninitialize(); + } + + protected override HttpResponse InitializeHttpResponse() + { + if (_pooledHttpResponse != null) + { + _pooledHttpResponse.Initialize(this); + return _pooledHttpResponse; + } + + return new DefaultHttpResponse(this); + } + + protected override void UninitializeHttpResponse(HttpResponse instance) + { + _pooledHttpResponse = instance as DefaultHttpResponse; + _pooledHttpResponse?.Uninitialize(); + } + } +} \ No newline at end of file diff --git a/src/Http/samples/SampleApp/PooledHttpContextFactory.cs b/src/Http/samples/SampleApp/PooledHttpContextFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..c61e139ac3af1b79151a461d0d374f32591ec457 --- /dev/null +++ b/src/Http/samples/SampleApp/PooledHttpContextFactory.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.ObjectPool; + +namespace SampleApp +{ + public class PooledHttpContextFactory : IHttpContextFactory + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly Stack<PooledHttpContext> _pool = new Stack<PooledHttpContext>(); + + public PooledHttpContextFactory(ObjectPoolProvider poolProvider) + : this(poolProvider, httpContextAccessor: null) + { + } + + public PooledHttpContextFactory(ObjectPoolProvider poolProvider, IHttpContextAccessor httpContextAccessor) + { + if (poolProvider == null) + { + throw new ArgumentNullException(nameof(poolProvider)); + } + + _httpContextAccessor = httpContextAccessor; + } + + public HttpContext Create(IFeatureCollection featureCollection) + { + if (featureCollection == null) + { + throw new ArgumentNullException(nameof(featureCollection)); + } + + PooledHttpContext httpContext = null; + lock (_pool) + { + if (_pool.Count != 0) + { + httpContext = _pool.Pop(); + } + } + + if (httpContext == null) + { + httpContext = new PooledHttpContext(featureCollection); + } + else + { + httpContext.Initialize(featureCollection); + } + + if (_httpContextAccessor != null) + { + _httpContextAccessor.HttpContext = httpContext; + } + return httpContext; + } + + public void Dispose(HttpContext httpContext) + { + if (_httpContextAccessor != null) + { + _httpContextAccessor.HttpContext = null; + } + + var pooled = httpContext as PooledHttpContext; + if (pooled != null) + { + pooled.Uninitialize(); + lock (_pool) + { + _pool.Push(pooled); + } + } + } + } +} diff --git a/src/Http/samples/SampleApp/Program.cs b/src/Http/samples/SampleApp/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..28d24befe016e67f2b4e98ee26704394ee082a6b --- /dev/null +++ b/src/Http/samples/SampleApp/Program.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; + +namespace SampleApp +{ + public class Program + { + public static void Main(string[] args) + { + var query = new QueryBuilder() + { + { "hello", "world" } + }.ToQueryString(); + + var uri = UriHelper.BuildAbsolute("http", new HostString("contoso.com"), query: query); + + Console.WriteLine(uri); + } + } +} diff --git a/src/Http/samples/SampleApp/SampleApp.csproj b/src/Http/samples/SampleApp/SampleApp.csproj new file mode 100644 index 0000000000000000000000000000000000000000..aedd176becb5ceb9b3dea74e581f2cf1ecc899e0 --- /dev/null +++ b/src/Http/samples/SampleApp/SampleApp.csproj @@ -0,0 +1,13 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks> + <OutputType>Exe</OutputType> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Http" /> + <Reference Include="Microsoft.AspNetCore.Http.Extensions" /> + </ItemGroup> + +</Project> diff --git a/src/Shared/Hosting.WebHostBuilderFactory/FactoryResolutionResult.cs b/src/Shared/Hosting.WebHostBuilderFactory/FactoryResolutionResult.cs new file mode 100644 index 0000000000000000000000000000000000000000..745cca7ebd44d3cebf457d9dbbcb0040f9082aba --- /dev/null +++ b/src/Shared/Hosting.WebHostBuilderFactory/FactoryResolutionResult.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Hosting.WebHostBuilderFactory +{ + internal class FactoryResolutionResult<TWebHost,TWebHostBuilder> + { + public FactoryResolutionResultKind ResultKind { get; set; } + public Type ProgramType { get; set; } + public Func<string[], TWebHost> WebHostFactory { get; set; } + public Func<string[], TWebHostBuilder> WebHostBuilderFactory { get; set; } + + internal static FactoryResolutionResult<TWebHost, TWebHostBuilder> NoBuildWebHost(Type programType) => + new FactoryResolutionResult<TWebHost, TWebHostBuilder> + { + ProgramType = programType, + ResultKind = FactoryResolutionResultKind.NoBuildWebHost + }; + + internal static FactoryResolutionResult<TWebHost, TWebHostBuilder> NoCreateWebHostBuilder(Type programType) => + new FactoryResolutionResult<TWebHost, TWebHostBuilder> + { + ProgramType = programType, + ResultKind = FactoryResolutionResultKind.NoCreateWebHostBuilder + }; + + internal static FactoryResolutionResult<TWebHost, TWebHostBuilder> NoEntryPoint() => + new FactoryResolutionResult<TWebHost, TWebHostBuilder> + { + ResultKind = FactoryResolutionResultKind.NoEntryPoint + }; + + internal static FactoryResolutionResult<TWebHost, TWebHostBuilder> Succeded(Func<string[], TWebHost> factory, Type programType) => new FactoryResolutionResult<TWebHost, TWebHostBuilder> + { + ProgramType = programType, + ResultKind = FactoryResolutionResultKind.Success, + WebHostFactory = factory + }; + + internal static FactoryResolutionResult<TWebHost, TWebHostBuilder> Succeded(Func<string[], TWebHostBuilder> factory, Type programType) => new FactoryResolutionResult<TWebHost, TWebHostBuilder> + { + ProgramType = programType, + ResultKind = FactoryResolutionResultKind.Success, + WebHostBuilderFactory = factory, + WebHostFactory = args => + { + var builder = factory(args); + return (TWebHost)builder.GetType().GetMethod("Build").Invoke(builder, Array.Empty<object>()); + } + }; + } +} diff --git a/src/Shared/Hosting.WebHostBuilderFactory/FactoryResolutionResultKind.cs b/src/Shared/Hosting.WebHostBuilderFactory/FactoryResolutionResultKind.cs new file mode 100644 index 0000000000000000000000000000000000000000..bb970d21a491125f393166f0c13012c7cf1755ca --- /dev/null +++ b/src/Shared/Hosting.WebHostBuilderFactory/FactoryResolutionResultKind.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +namespace Microsoft.AspNetCore.Hosting.WebHostBuilderFactory +{ + internal enum FactoryResolutionResultKind + { + Success, + NoEntryPoint, + NoCreateWebHostBuilder, + NoBuildWebHost + } +} diff --git a/src/Shared/Hosting.WebHostBuilderFactory/WebHostFactoryResolver.cs b/src/Shared/Hosting.WebHostBuilderFactory/WebHostFactoryResolver.cs new file mode 100644 index 0000000000000000000000000000000000000000..916b15e020a4fdfb34c988f9e0417a9df7e83044 --- /dev/null +++ b/src/Shared/Hosting.WebHostBuilderFactory/WebHostFactoryResolver.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; + +namespace Microsoft.AspNetCore.Hosting.WebHostBuilderFactory +{ + internal class WebHostFactoryResolver + { + public static readonly string CreateWebHostBuilder = nameof(CreateWebHostBuilder); + public static readonly string BuildWebHost = nameof(BuildWebHost); + + public static FactoryResolutionResult<TWebhost,TWebhostBuilder> ResolveWebHostBuilderFactory<TWebhost, TWebhostBuilder>(Assembly assembly) + { + var programType = assembly?.EntryPoint?.DeclaringType; + if (programType == null) + { + return FactoryResolutionResult<TWebhost, TWebhostBuilder>.NoEntryPoint(); + } + + var factory = programType.GetTypeInfo().GetDeclaredMethod(CreateWebHostBuilder); + if (factory == null || + !typeof(TWebhostBuilder).IsAssignableFrom(factory.ReturnType) || + factory.GetParameters().Length != 1 || + !typeof(string []).Equals(factory.GetParameters()[0].ParameterType)) + { + return FactoryResolutionResult<TWebhost, TWebhostBuilder>.NoCreateWebHostBuilder(programType); + } + + return FactoryResolutionResult<TWebhost, TWebhostBuilder>.Succeded(args => (TWebhostBuilder)factory.Invoke(null, new object[] { args }), programType); + } + + public static FactoryResolutionResult<TWebhost, TWebhostBuilder> ResolveWebHostFactory<TWebhost, TWebhostBuilder>(Assembly assembly) + { + // We want to give priority to BuildWebHost over CreateWebHostBuilder for backwards + // compatibility with existing projects that follow the old pattern. + var findResult = ResolveWebHostBuilderFactory<TWebhost, TWebhostBuilder>(assembly); + switch (findResult.ResultKind) + { + case FactoryResolutionResultKind.NoEntryPoint: + return findResult; + case FactoryResolutionResultKind.Success: + case FactoryResolutionResultKind.NoCreateWebHostBuilder: + var buildWebHostMethod = findResult.ProgramType.GetTypeInfo().GetDeclaredMethod(BuildWebHost); + if (buildWebHostMethod == null || + !typeof(TWebhost).IsAssignableFrom(buildWebHostMethod.ReturnType) || + buildWebHostMethod.GetParameters().Length != 1 || + !typeof(string[]).Equals(buildWebHostMethod.GetParameters()[0].ParameterType)) + { + if (findResult.ResultKind == FactoryResolutionResultKind.Success) + { + return findResult; + } + + return FactoryResolutionResult<TWebhost, TWebhostBuilder>.NoBuildWebHost(findResult.ProgramType); + } + else + { + return FactoryResolutionResult<TWebhost, TWebhostBuilder>.Succeded(args => (TWebhost)buildWebHostMethod.Invoke(null, new object[] { args }), findResult.ProgramType); + } + case FactoryResolutionResultKind.NoBuildWebHost: + default: + throw new InvalidOperationException(); + } + } + } +} diff --git a/src/templating/.gitignore b/src/Templating/.gitignore similarity index 100% rename from src/templating/.gitignore rename to src/Templating/.gitignore diff --git a/src/templating/Directory.Build.props b/src/Templating/Directory.Build.props similarity index 91% rename from src/templating/Directory.Build.props rename to src/Templating/Directory.Build.props index 92ebf42a309139657e03668aff48acfb6bfc4cca..9887edd5d59d8f7a7ba186cede1b8b89e5221aad 100644 --- a/src/templating/Directory.Build.props +++ b/src/Templating/Directory.Build.props @@ -10,7 +10,7 @@ <PropertyGroup> <Product>Microsoft ASP.NET Core</Product> <RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot> - <RepositoryUrl>https://github.com/aspnet/templating</RepositoryUrl> + <RepositoryUrl>https://github.com/aspnet/Templating</RepositoryUrl> <RepositoryType>git</RepositoryType> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> diff --git a/src/templating/Directory.Build.targets b/src/Templating/Directory.Build.targets similarity index 100% rename from src/templating/Directory.Build.targets rename to src/Templating/Directory.Build.targets diff --git a/src/templating/NuGetPackageVerifier.json b/src/Templating/NuGetPackageVerifier.json similarity index 100% rename from src/templating/NuGetPackageVerifier.json rename to src/Templating/NuGetPackageVerifier.json diff --git a/src/templating/README.md b/src/Templating/README.md similarity index 100% rename from src/templating/README.md rename to src/Templating/README.md diff --git a/src/templating/Templating.sln b/src/Templating/Templating.sln similarity index 100% rename from src/templating/Templating.sln rename to src/Templating/Templating.sln diff --git a/src/templating/build/dependencies.props b/src/Templating/build/dependencies.props similarity index 100% rename from src/templating/build/dependencies.props rename to src/Templating/build/dependencies.props diff --git a/src/templating/build/repo.props b/src/Templating/build/repo.props similarity index 100% rename from src/templating/build/repo.props rename to src/Templating/build/repo.props diff --git a/src/templating/build/sources.props b/src/Templating/build/sources.props similarity index 100% rename from src/templating/build/sources.props rename to src/Templating/build/sources.props diff --git a/src/templating/src/Directory.Build.props b/src/Templating/src/Directory.Build.props similarity index 100% rename from src/templating/src/Directory.Build.props rename to src/Templating/src/Directory.Build.props diff --git a/src/templating/src/Directory.Build.targets b/src/Templating/src/Directory.Build.targets similarity index 100% rename from src/templating/src/Directory.Build.targets rename to src/Templating/src/Directory.Build.targets diff --git a/src/templating/src/GenerateContent.targets b/src/Templating/src/GenerateContent.targets similarity index 100% rename from src/templating/src/GenerateContent.targets rename to src/Templating/src/GenerateContent.targets diff --git a/src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/Microsoft.DotNet.Web.Client.ItemTemplates.csproj b/src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/Microsoft.DotNet.Web.Client.ItemTemplates.csproj similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/Microsoft.DotNet.Web.Client.ItemTemplates.csproj rename to src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/Microsoft.DotNet.Web.Client.ItemTemplates.csproj diff --git a/src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Less/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Less/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Less/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Less/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Less/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Less/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Less/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Less/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Less/styleSheet1.less b/src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Less/styleSheet1.less similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Less/styleSheet1.less rename to src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Less/styleSheet1.less diff --git a/src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Scss/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Scss/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Scss/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Scss/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Scss/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Scss/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Scss/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Scss/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Scss/styleSheet1.scss b/src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Scss/styleSheet1.scss similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Scss/styleSheet1.scss rename to src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/Scss/styleSheet1.scss diff --git a/src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/TypeScript/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/TypeScript/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/TypeScript/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/TypeScript/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/TypeScript/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/TypeScript/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/TypeScript/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/TypeScript/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/TypeScript/file1.ts b/src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/TypeScript/file1.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/TypeScript/file1.ts rename to src/Templating/src/Microsoft.DotNet.Web.Client.ItemTemplates/content/TypeScript/file1.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.ItemTemplates/Microsoft.DotNet.Web.ItemTemplates.csproj b/src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/Microsoft.DotNet.Web.ItemTemplates.csproj similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ItemTemplates/Microsoft.DotNet.Web.ItemTemplates.csproj rename to src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/Microsoft.DotNet.Web.ItemTemplates.csproj diff --git a/src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/Index.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/Index.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/Index.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/Index.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/Index.cshtml.cs b/src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/Index.cshtml.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/Index.cshtml.cs rename to src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/RazorPage/Index.cshtml.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewImports/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewImports/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewImports/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewImports/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewImports/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewImports/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewImports/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewImports/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewImports/_ViewImports.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewImports/_ViewImports.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewImports/_ViewImports.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewImports/_ViewImports.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewStart/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewStart/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewStart/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewStart/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewStart/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewStart/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewStart/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewStart/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewStart/_ViewStart.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewStart/_ViewStart.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewStart/_ViewStart.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ItemTemplates/content/ViewStart/_ViewStart.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/.gitignore b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/.gitignore similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/.gitignore rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/.gitignore diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/EmptyWeb-CSharp.csproj.in b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/EmptyWeb-CSharp.csproj.in similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/EmptyWeb-CSharp.csproj.in rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/EmptyWeb-CSharp.csproj.in diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/EmptyWeb-FSharp.fsproj.in b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/EmptyWeb-FSharp.fsproj.in similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/EmptyWeb-FSharp.fsproj.in rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/EmptyWeb-FSharp.fsproj.in diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorClassLibrary-CSharp.csproj.in b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorClassLibrary-CSharp.csproj.in similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorClassLibrary-CSharp.csproj.in rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorClassLibrary-CSharp.csproj.in diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorPagesWeb-CSharp.csproj.in b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorPagesWeb-CSharp.csproj.in similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorPagesWeb-CSharp.csproj.in rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorPagesWeb-CSharp.csproj.in diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/StarterWeb-CSharp.csproj.in b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/StarterWeb-CSharp.csproj.in similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/StarterWeb-CSharp.csproj.in rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/StarterWeb-CSharp.csproj.in diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/StarterWeb-FSharp.fsproj.in b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/StarterWeb-FSharp.fsproj.in similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/StarterWeb-FSharp.fsproj.in rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/StarterWeb-FSharp.fsproj.in diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/WebApi-CSharp.csproj.in b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/WebApi-CSharp.csproj.in similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/WebApi-CSharp.csproj.in rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/WebApi-CSharp.csproj.in diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/WebApi-FSharp.fsproj.in b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/WebApi-FSharp.fsproj.in similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/WebApi-FSharp.fsproj.in rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/WebApi-FSharp.fsproj.in diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/Directory.Build.props b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/Directory.Build.props similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/Directory.Build.props rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/Directory.Build.props diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/Directory.Build.targets b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/Directory.Build.targets similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/Directory.Build.targets rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/Directory.Build.targets diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/vs-2017.3.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/vs-2017.3.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/vs-2017.3.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/vs-2017.3.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/vs-2017.3/Empty.png b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/vs-2017.3/Empty.png similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/vs-2017.3/Empty.png rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/vs-2017.3/Empty.png diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/Program.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/Program.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/Program.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/Program.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/Properties/launchSettings.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/Properties/launchSettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/Properties/launchSettings.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/Properties/launchSettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/Startup.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/Startup.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/Startup.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/Startup.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/app.config b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/app.config similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/app.config rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/app.config diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/wwwroot/-.- b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/wwwroot/-.- similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/wwwroot/-.- rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/wwwroot/-.- diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/vs-2017.3.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/vs-2017.3.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/vs-2017.3.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/vs-2017.3.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/vs-2017.3/Empty.png b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/vs-2017.3/Empty.png similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/vs-2017.3/Empty.png rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/vs-2017.3/Empty.png diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/Program.fs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/Program.fs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/Program.fs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/Program.fs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/Properties/launchSettings.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/Properties/launchSettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/Properties/launchSettings.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/Properties/launchSettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/Startup.fs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/Startup.fs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/Startup.fs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/Startup.fs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/app.config b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/app.config similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/app.config rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-FSharp/app.config diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/vs-2017.3.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/vs-2017.3.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/vs-2017.3.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/vs-2017.3.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/vs-2017.3/RazorClassLibrary.ico b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/vs-2017.3/RazorClassLibrary.ico similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/vs-2017.3/RazorClassLibrary.ico rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/vs-2017.3/RazorClassLibrary.ico diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/Areas/MyFeature/Pages/Page1.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/Areas/MyFeature/Pages/Page1.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/Areas/MyFeature/Pages/Page1.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/Areas/MyFeature/Pages/Page1.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/Areas/MyFeature/Pages/Page1.cshtml.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/Areas/MyFeature/Pages/Page1.cshtml.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/Areas/MyFeature/Pages/Page1.cshtml.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorClassLibrary-CSharp/Areas/MyFeature/Pages/Page1.cshtml.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/vs-2017.3.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/vs-2017.3.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/vs-2017.3.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/vs-2017.3.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/vs-2017.3/WebApplication.png b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/vs-2017.3/WebApplication.png similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/vs-2017.3/WebApplication.png rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/vs-2017.3/WebApplication.png diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Areas/Identity/Pages/_ViewStart.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Areas/Identity/Pages/_ViewStart.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Areas/Identity/Pages/_ViewStart.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Areas/Identity/Pages/_ViewStart.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/ApplicationDbContext.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/ApplicationDbContext.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/ApplicationDbContext.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/ApplicationDbContext.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.Designer.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.Designer.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.Designer.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.Designer.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlLite/ApplicationDbContextModelSnapshot.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlLite/ApplicationDbContextModelSnapshot.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlLite/ApplicationDbContextModelSnapshot.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlLite/ApplicationDbContextModelSnapshot.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.Designer.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.Designer.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.Designer.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.Designer.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlServer/ApplicationDbContextModelSnapshot.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlServer/ApplicationDbContextModelSnapshot.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlServer/ApplicationDbContextModelSnapshot.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Data/SqlServer/ApplicationDbContextModelSnapshot.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/About.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/About.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/About.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/About.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/About.cshtml.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/About.cshtml.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/About.cshtml.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/About.cshtml.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Contact.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Contact.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Contact.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Contact.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Contact.cshtml.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Contact.cshtml.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Contact.cshtml.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Contact.cshtml.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Error.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Error.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Error.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Error.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Error.cshtml.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Error.cshtml.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Error.cshtml.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Error.cshtml.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Index.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Index.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Index.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Index.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Index.cshtml.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Index.cshtml.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Index.cshtml.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Index.cshtml.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Privacy.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Privacy.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Privacy.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Privacy.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Privacy.cshtml.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Privacy.cshtml.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Privacy.cshtml.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Privacy.cshtml.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_CookieConsentPartial.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_CookieConsentPartial.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_CookieConsentPartial.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_CookieConsentPartial.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_Layout.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_Layout.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_Layout.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_Layout.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_LoginPartial.Identity.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_LoginPartial.Identity.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_LoginPartial.Identity.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_LoginPartial.Identity.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_LoginPartial.OrgAuth.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_LoginPartial.OrgAuth.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_LoginPartial.OrgAuth.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_LoginPartial.OrgAuth.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_ValidationScriptsPartial.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_ValidationScriptsPartial.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_ValidationScriptsPartial.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_ValidationScriptsPartial.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/_ViewImports.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/_ViewImports.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/_ViewImports.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/_ViewImports.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/_ViewStart.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/_ViewStart.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/_ViewStart.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/_ViewStart.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Properties/launchSettings.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Properties/launchSettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Properties/launchSettings.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Properties/launchSettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Startup.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Startup.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Startup.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Startup.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/app.config b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/app.config similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/app.config rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/app.config diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/app.db b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/app.db similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/app.db rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/app.db diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/appsettings.Development.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/appsettings.Development.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/appsettings.Development.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/appsettings.Development.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/appsettings.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/appsettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/appsettings.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/appsettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/css/site.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/css/site.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/css/site.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/css/site.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/css/site.min.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/css/site.min.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/css/site.min.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/css/site.min.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/favicon.ico b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/favicon.ico similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/favicon.ico rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/favicon.ico diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/images/banner1.svg b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/images/banner1.svg similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/images/banner1.svg rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/images/banner1.svg diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/images/banner2.svg b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/images/banner2.svg similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/images/banner2.svg rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/images/banner2.svg diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/images/banner3.svg b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/images/banner3.svg similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/images/banner3.svg rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/images/banner3.svg diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/.bower.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/.bower.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/.bower.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/.bower.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/LICENSE b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/LICENSE similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/LICENSE rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/LICENSE diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/js/npm.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/js/npm.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/js/npm.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/bootstrap/dist/js/npm.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/.bower.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/.bower.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/.bower.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/.bower.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/.bower.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/.bower.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/.bower.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/.bower.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/LICENSE.md b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/LICENSE.md similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/LICENSE.md rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/LICENSE.md diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/.bower.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/.bower.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/.bower.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/.bower.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/LICENSE.txt b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/LICENSE.txt similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/LICENSE.txt rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/LICENSE.txt diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/dist/jquery.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/dist/jquery.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/dist/jquery.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/dist/jquery.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/vs-2017.3.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/vs-2017.3.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/vs-2017.3.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/vs-2017.3.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/vs-2017.3/WebApplication.png b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/vs-2017.3/WebApplication.png similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/vs-2017.3/WebApplication.png rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/vs-2017.3/WebApplication.png diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Areas/Identity/Pages/_ViewStart.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Areas/Identity/Pages/_ViewStart.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Areas/Identity/Pages/_ViewStart.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Areas/Identity/Pages/_ViewStart.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Controllers/HomeController.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Controllers/HomeController.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Controllers/HomeController.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Controllers/HomeController.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/ApplicationDbContext.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/ApplicationDbContext.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/ApplicationDbContext.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/ApplicationDbContext.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.Designer.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.Designer.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.Designer.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.Designer.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlLite/ApplicationDbContextModelSnapshot.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlLite/ApplicationDbContextModelSnapshot.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlLite/ApplicationDbContextModelSnapshot.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlLite/ApplicationDbContextModelSnapshot.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.Designer.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.Designer.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.Designer.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.Designer.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlServer/ApplicationDbContextModelSnapshot.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlServer/ApplicationDbContextModelSnapshot.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlServer/ApplicationDbContextModelSnapshot.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Data/SqlServer/ApplicationDbContextModelSnapshot.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Models/ErrorViewModel.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Models/ErrorViewModel.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Models/ErrorViewModel.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Models/ErrorViewModel.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Program.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Program.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Program.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Program.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Properties/launchSettings.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Properties/launchSettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Properties/launchSettings.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Properties/launchSettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Startup.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Startup.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Startup.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Startup.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/About.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/About.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/About.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/About.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/Contact.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/Contact.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/Contact.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/Contact.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/Index.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/Index.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/Index.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/Index.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/Privacy.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/Privacy.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/Privacy.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Home/Privacy.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/Error.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/Error.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/Error.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/Error.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_CookieConsentPartial.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_CookieConsentPartial.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_CookieConsentPartial.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_CookieConsentPartial.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_LoginPartial.Identity.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_LoginPartial.Identity.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_LoginPartial.Identity.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_LoginPartial.Identity.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_LoginPartial.OrgAuth.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_LoginPartial.OrgAuth.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_LoginPartial.OrgAuth.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_LoginPartial.OrgAuth.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_ValidationScriptsPartial.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_ValidationScriptsPartial.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_ValidationScriptsPartial.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_ValidationScriptsPartial.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/_ViewImports.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/_ViewImports.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/_ViewImports.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/_ViewImports.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/_ViewStart.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/_ViewStart.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/_ViewStart.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/_ViewStart.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/app.config b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/app.config similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/app.config rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/app.config diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/app.db b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/app.db similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/app.db rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/app.db diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/appsettings.Development.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/appsettings.Development.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/appsettings.Development.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/appsettings.Development.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/appsettings.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/appsettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/appsettings.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/appsettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/css/site.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/css/site.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/css/site.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/css/site.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/css/site.min.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/css/site.min.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/css/site.min.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/css/site.min.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/favicon.ico b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/favicon.ico similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/favicon.ico rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/favicon.ico diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/images/banner1.svg b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/images/banner1.svg similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/images/banner1.svg rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/images/banner1.svg diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/images/banner2.svg b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/images/banner2.svg similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/images/banner2.svg rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/images/banner2.svg diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/images/banner3.svg b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/images/banner3.svg similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/images/banner3.svg rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/images/banner3.svg diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/js/site.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/js/site.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/js/site.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/js/site.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/js/site.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/js/site.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/js/site.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/js/site.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/.bower.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/.bower.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/.bower.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/.bower.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/LICENSE b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/LICENSE similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/LICENSE rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/LICENSE diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/js/npm.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/js/npm.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/js/npm.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/bootstrap/dist/js/npm.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/.bower.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/.bower.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/.bower.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/.bower.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/.bower.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/.bower.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/.bower.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/.bower.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/LICENSE.md b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/LICENSE.md similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/LICENSE.md rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/LICENSE.md diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/.bower.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/.bower.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/.bower.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/.bower.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/LICENSE.txt b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/LICENSE.txt similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/LICENSE.txt rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/LICENSE.txt diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/dist/jquery.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/dist/jquery.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/dist/jquery.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/dist/jquery.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/lib/jquery/dist/jquery.min.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/.bowerrc b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/.bowerrc similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/.bowerrc rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/.bowerrc diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Controllers/HomeController.fs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Controllers/HomeController.fs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Controllers/HomeController.fs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Controllers/HomeController.fs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Models/ErrorViewModel.fs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Models/ErrorViewModel.fs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Models/ErrorViewModel.fs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Models/ErrorViewModel.fs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Program.fs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Program.fs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Program.fs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Program.fs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Properties/launchSettings.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Properties/launchSettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Properties/launchSettings.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Properties/launchSettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Startup.fs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Startup.fs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Startup.fs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Startup.fs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Home/About.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Home/About.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Home/About.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Home/About.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Home/Contact.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Home/Contact.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Home/Contact.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Home/Contact.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Home/Index.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Home/Index.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Home/Index.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Home/Index.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Shared/Error.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Shared/Error.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Shared/Error.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Shared/Error.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Shared/_Layout.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Shared/_Layout.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Shared/_Layout.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Shared/_Layout.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Shared/_ValidationScriptsPartial.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Shared/_ValidationScriptsPartial.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Shared/_ValidationScriptsPartial.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/Shared/_ValidationScriptsPartial.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/_ViewImports.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/_ViewImports.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/_ViewImports.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/_ViewImports.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/_ViewStart.cshtml b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/_ViewStart.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/_ViewStart.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/Views/_ViewStart.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/app.config b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/app.config similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/app.config rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/app.config diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/appsettings.Development.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/appsettings.Development.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/appsettings.Development.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/appsettings.Development.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/appsettings.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/appsettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/appsettings.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/appsettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/css/site.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/css/site.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/css/site.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/css/site.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/css/site.min.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/css/site.min.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/css/site.min.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/css/site.min.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/favicon.ico b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/favicon.ico similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/favicon.ico rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/favicon.ico diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/images/banner1.svg b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/images/banner1.svg similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/images/banner1.svg rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/images/banner1.svg diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/images/banner2.svg b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/images/banner2.svg similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/images/banner2.svg rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/images/banner2.svg diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/images/banner3.svg b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/images/banner3.svg similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/images/banner3.svg rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/images/banner3.svg diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/.bower.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/.bower.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/.bower.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/.bower.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/LICENSE b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/LICENSE similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/LICENSE rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/LICENSE diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/js/npm.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/js/npm.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/js/npm.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/bootstrap/dist/js/npm.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/.bower.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/.bower.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/.bower.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/.bower.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/.bower.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/.bower.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/.bower.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/.bower.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/LICENSE.md b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/LICENSE.md similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/LICENSE.md rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/LICENSE.md diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/additional-methods.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/additional-methods.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/additional-methods.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/additional-methods.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/.bower.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/.bower.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/.bower.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/.bower.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/LICENSE.txt b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/LICENSE.txt similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/LICENSE.txt rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/LICENSE.txt diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/dist/jquery.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/dist/jquery.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/dist/jquery.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/dist/jquery.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/dist/jquery.min.js b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/dist/jquery.min.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/dist/jquery.min.js rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/dist/jquery.min.js diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/dist/jquery.min.map b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/dist/jquery.min.map similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/dist/jquery.min.map rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/lib/jquery/dist/jquery.min.map diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/vs-2017.3.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/vs-2017.3.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/vs-2017.3.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/vs-2017.3.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/vs-2017.3/WebAPI.png b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/vs-2017.3/WebAPI.png similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/vs-2017.3/WebAPI.png rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/.template.config/vs-2017.3/WebAPI.png diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Controllers/ValuesController.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Controllers/ValuesController.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Controllers/ValuesController.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Controllers/ValuesController.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Program.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Program.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Program.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Program.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Properties/launchSettings.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Properties/launchSettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Properties/launchSettings.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Properties/launchSettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Startup.cs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Startup.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Startup.cs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/Startup.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/app.config b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/app.config similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/app.config rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/app.config diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/appsettings.Development.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/appsettings.Development.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/appsettings.Development.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/appsettings.Development.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/appsettings.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/appsettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/appsettings.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/appsettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/wwwroot/-.- b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/wwwroot/-.- similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/wwwroot/-.- rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-CSharp/wwwroot/-.- diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/vs-2017.3.host.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/vs-2017.3.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/vs-2017.3.host.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/vs-2017.3.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/vs-2017.3/WebAPI.png b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/vs-2017.3/WebAPI.png similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/vs-2017.3/WebAPI.png rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/.template.config/vs-2017.3/WebAPI.png diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Controllers/ValuesController.fs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Controllers/ValuesController.fs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Controllers/ValuesController.fs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Controllers/ValuesController.fs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Program.fs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Program.fs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Program.fs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Program.fs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Properties/launchSettings.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Properties/launchSettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Properties/launchSettings.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Properties/launchSettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Startup.fs b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Startup.fs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Startup.fs rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/Startup.fs diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/app.config b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/app.config similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/app.config rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/app.config diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/appsettings.Development.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/appsettings.Development.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/appsettings.Development.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/appsettings.Development.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/appsettings.json b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/appsettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/appsettings.json rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/appsettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/wwwroot/-.- b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/wwwroot/-.- similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/wwwroot/-.- rename to src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/WebApi-FSharp/wwwroot/-.- diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/.gitignore b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/.gitignore similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/.gitignore rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/.gitignore diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/Angular-CSharp.csproj.in b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/Angular-CSharp.csproj.in similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/Angular-CSharp.csproj.in rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/Angular-CSharp.csproj.in diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/Microsoft.DotNet.Web.Spa.ProjectTemplates.csproj b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/Microsoft.DotNet.Web.Spa.ProjectTemplates.csproj similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/Microsoft.DotNet.Web.Spa.ProjectTemplates.csproj rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/Microsoft.DotNet.Web.Spa.ProjectTemplates.csproj diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/React-CSharp.csproj.in b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/React-CSharp.csproj.in similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/React-CSharp.csproj.in rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/React-CSharp.csproj.in diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/ReactRedux-CSharp.csproj.in b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/ReactRedux-CSharp.csproj.in similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/ReactRedux-CSharp.csproj.in rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/ReactRedux-CSharp.csproj.in diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.gitignore b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.gitignore similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.gitignore rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.gitignore diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/icon.png b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/icon.png similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/icon.png rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/icon.png diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/vs-2017.3.host.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/vs-2017.3.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/vs-2017.3.host.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/vs-2017.3.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/.angular-cli.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/.angular-cli.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/.angular-cli.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/.angular-cli.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/.editorconfig b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/.editorconfig similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/.editorconfig rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/.editorconfig diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/.gitignore b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/.gitignore similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/.gitignore rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/.gitignore diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/README.md b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/README.md similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/README.md rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/README.md diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/e2e/app.e2e-spec.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/e2e/app.e2e-spec.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/e2e/app.e2e-spec.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/e2e/app.e2e-spec.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/e2e/app.po.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/e2e/app.po.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/e2e/app.po.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/e2e/app.po.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/e2e/tsconfig.e2e.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/e2e/tsconfig.e2e.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/e2e/tsconfig.e2e.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/e2e/tsconfig.e2e.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/karma.conf.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/karma.conf.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/karma.conf.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/karma.conf.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/package-lock.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/package-lock.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/package-lock.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/package-lock.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/package.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/package.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/package.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/package.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/protractor.conf.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/protractor.conf.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/protractor.conf.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/protractor.conf.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.component.css b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.component.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.component.css rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.component.css diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.component.html b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.component.html similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.component.html rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.component.html diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.component.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.component.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.component.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.component.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.module.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.module.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.module.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/app.module.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.html b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.html similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.html rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.html diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.spec.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.spec.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.spec.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.spec.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/fetch-data/fetch-data.component.html b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/fetch-data/fetch-data.component.html similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/fetch-data/fetch-data.component.html rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/fetch-data/fetch-data.component.html diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/fetch-data/fetch-data.component.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/fetch-data/fetch-data.component.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/fetch-data/fetch-data.component.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/fetch-data/fetch-data.component.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/home/home.component.html b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/home/home.component.html similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/home/home.component.html rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/home/home.component.html diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/home/home.component.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/home/home.component.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/home/home.component.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/home/home.component.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/nav-menu/nav-menu.component.css b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/nav-menu/nav-menu.component.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/nav-menu/nav-menu.component.css rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/nav-menu/nav-menu.component.css diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/nav-menu/nav-menu.component.html b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/nav-menu/nav-menu.component.html similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/nav-menu/nav-menu.component.html rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/nav-menu/nav-menu.component.html diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/nav-menu/nav-menu.component.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/nav-menu/nav-menu.component.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/nav-menu/nav-menu.component.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/nav-menu/nav-menu.component.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/assets/.gitkeep b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/assets/.gitkeep similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/assets/.gitkeep rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/assets/.gitkeep diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/environments/environment.prod.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/environments/environment.prod.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/environments/environment.prod.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/environments/environment.prod.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/environments/environment.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/environments/environment.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/environments/environment.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/environments/environment.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/index.html b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/index.html similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/index.html rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/index.html diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/main.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/main.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/main.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/main.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/polyfills.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/polyfills.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/polyfills.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/polyfills.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/styles.css b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/styles.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/styles.css rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/styles.css diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/test.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/test.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/test.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/test.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/tsconfig.app.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/tsconfig.app.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/tsconfig.app.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/tsconfig.app.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/tsconfig.spec.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/tsconfig.spec.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/tsconfig.spec.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/tsconfig.spec.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/typings.d.ts b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/typings.d.ts similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/typings.d.ts rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/typings.d.ts diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/tsconfig.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/tsconfig.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/tsconfig.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/tsconfig.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/tslint.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/tslint.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/tslint.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/tslint.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Controllers/SampleDataController.cs b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Controllers/SampleDataController.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Controllers/SampleDataController.cs rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Controllers/SampleDataController.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Pages/Error.cshtml b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Pages/Error.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Pages/Error.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Pages/Error.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Pages/Error.cshtml.cs b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Pages/Error.cshtml.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Pages/Error.cshtml.cs rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Pages/Error.cshtml.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Pages/_ViewImports.cshtml b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Pages/_ViewImports.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Pages/_ViewImports.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Pages/_ViewImports.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Program.cs b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Program.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Program.cs rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Program.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Properties/launchSettings.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Properties/launchSettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Properties/launchSettings.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Properties/launchSettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Startup.cs b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Startup.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Startup.cs rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/Startup.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/app.config b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/app.config similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/app.config rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/app.config diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/appsettings.Development.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/appsettings.Development.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/appsettings.Development.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/appsettings.Development.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/appsettings.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/appsettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/appsettings.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/appsettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/wwwroot/favicon.ico b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/wwwroot/favicon.ico similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/wwwroot/favicon.ico rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Angular-CSharp/wwwroot/favicon.ico diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Directory.Build.props b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Directory.Build.props similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Directory.Build.props rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Directory.Build.props diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Directory.Build.targets b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Directory.Build.targets similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Directory.Build.targets rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/Directory.Build.targets diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.gitignore b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.gitignore similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.gitignore rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.gitignore diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/icon.png b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/icon.png similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/icon.png rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/icon.png diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/vs-2017.3.host.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/vs-2017.3.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/vs-2017.3.host.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/vs-2017.3.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/.gitignore b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/.gitignore similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/.gitignore rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/.gitignore diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/README.md b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/README.md similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/README.md rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/README.md diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/package-lock.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/package-lock.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/package-lock.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/package-lock.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/package.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/package.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/package.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/package.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/public/favicon.ico b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/public/favicon.ico similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/public/favicon.ico rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/public/favicon.ico diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/public/index.html b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/public/index.html similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/public/index.html rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/public/index.html diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/public/manifest.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/public/manifest.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/public/manifest.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/public/manifest.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/App.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/App.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/App.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/App.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/App.test.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/App.test.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/App.test.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/App.test.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Counter.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Counter.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Counter.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Counter.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/FetchData.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/FetchData.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/FetchData.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/FetchData.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Home.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Home.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Home.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Home.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Layout.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Layout.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Layout.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Layout.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/NavMenu.css b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/NavMenu.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/NavMenu.css rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/NavMenu.css diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/NavMenu.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/NavMenu.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/NavMenu.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/NavMenu.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/index.css b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/index.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/index.css rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/index.css diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/index.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/index.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/index.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/index.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/registerServiceWorker.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/registerServiceWorker.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/registerServiceWorker.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/registerServiceWorker.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Controllers/SampleDataController.cs b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Controllers/SampleDataController.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Controllers/SampleDataController.cs rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Controllers/SampleDataController.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Pages/Error.cshtml b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Pages/Error.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Pages/Error.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Pages/Error.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Pages/Error.cshtml.cs b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Pages/Error.cshtml.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Pages/Error.cshtml.cs rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Pages/Error.cshtml.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Pages/_ViewImports.cshtml b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Pages/_ViewImports.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Pages/_ViewImports.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Pages/_ViewImports.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Program.cs b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Program.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Program.cs rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Program.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Properties/launchSettings.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Properties/launchSettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Properties/launchSettings.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Properties/launchSettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Startup.cs b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Startup.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Startup.cs rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Startup.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/app.config b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/app.config similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/app.config rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/app.config diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/appsettings.Development.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/appsettings.Development.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/appsettings.Development.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/appsettings.Development.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/appsettings.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/appsettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/appsettings.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/appsettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.gitignore b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.gitignore similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.gitignore rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.gitignore diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/dotnetcli.host.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/dotnetcli.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/dotnetcli.host.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/dotnetcli.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/icon.png b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/icon.png similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/icon.png rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/icon.png diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/template.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/template.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/template.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/template.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/vs-2017.3.host.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/vs-2017.3.host.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/vs-2017.3.host.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/vs-2017.3.host.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/.gitignore b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/.gitignore similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/.gitignore rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/.gitignore diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/README.md b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/README.md similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/README.md rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/README.md diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/package-lock.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/package-lock.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/package-lock.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/package-lock.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/package.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/package.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/package.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/package.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/public/favicon.ico b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/public/favicon.ico similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/public/favicon.ico rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/public/favicon.ico diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/public/index.html b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/public/index.html similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/public/index.html rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/public/index.html diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/public/manifest.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/public/manifest.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/public/manifest.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/public/manifest.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/App.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/App.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/App.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/App.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/App.test.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/App.test.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/App.test.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/App.test.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Counter.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Counter.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Counter.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Counter.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/FetchData.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/FetchData.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/FetchData.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/FetchData.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Home.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Home.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Home.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Home.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Layout.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Layout.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Layout.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Layout.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/NavMenu.css b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/NavMenu.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/NavMenu.css rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/NavMenu.css diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/NavMenu.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/NavMenu.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/NavMenu.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/NavMenu.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/index.css b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/index.css similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/index.css rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/index.css diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/index.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/index.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/index.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/index.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/registerServiceWorker.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/registerServiceWorker.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/registerServiceWorker.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/registerServiceWorker.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/Counter.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/Counter.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/Counter.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/Counter.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/WeatherForecasts.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/WeatherForecasts.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/WeatherForecasts.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/WeatherForecasts.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/configureStore.js b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/configureStore.js similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/configureStore.js rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/configureStore.js diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Controllers/SampleDataController.cs b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Controllers/SampleDataController.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Controllers/SampleDataController.cs rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Controllers/SampleDataController.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Pages/Error.cshtml b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Pages/Error.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Pages/Error.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Pages/Error.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Pages/Error.cshtml.cs b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Pages/Error.cshtml.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Pages/Error.cshtml.cs rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Pages/Error.cshtml.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Pages/_ViewImports.cshtml b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Pages/_ViewImports.cshtml similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Pages/_ViewImports.cshtml rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Pages/_ViewImports.cshtml diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Program.cs b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Program.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Program.cs rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Program.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Properties/launchSettings.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Properties/launchSettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Properties/launchSettings.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Properties/launchSettings.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Startup.cs b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Startup.cs similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Startup.cs rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Startup.cs diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/app.config b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/app.config similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/app.config rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/app.config diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/appsettings.Development.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/appsettings.Development.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/appsettings.Development.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/appsettings.Development.json diff --git a/src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/appsettings.json b/src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/appsettings.json similarity index 100% rename from src/templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/appsettings.json rename to src/Templating/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/appsettings.json diff --git a/src/templating/src/SetPackageProperties.targets b/src/Templating/src/SetPackageProperties.targets similarity index 100% rename from src/templating/src/SetPackageProperties.targets rename to src/Templating/src/SetPackageProperties.targets diff --git a/src/templating/src/THIRD-PARTY-NOTICES b/src/Templating/src/THIRD-PARTY-NOTICES similarity index 100% rename from src/templating/src/THIRD-PARTY-NOTICES rename to src/Templating/src/THIRD-PARTY-NOTICES diff --git a/src/templating/src/templates.nuspec b/src/Templating/src/templates.nuspec similarity index 100% rename from src/templating/src/templates.nuspec rename to src/Templating/src/templates.nuspec diff --git a/src/templating/test/Directory.Build.targets b/src/Templating/test/Directory.Build.targets similarity index 100% rename from src/templating/test/Directory.Build.targets rename to src/Templating/test/Directory.Build.targets diff --git a/src/templating/test/DotNetToolsInstaller/DotNetToolsInstaller.csproj b/src/Templating/test/DotNetToolsInstaller/DotNetToolsInstaller.csproj similarity index 100% rename from src/templating/test/DotNetToolsInstaller/DotNetToolsInstaller.csproj rename to src/Templating/test/DotNetToolsInstaller/DotNetToolsInstaller.csproj diff --git a/src/templating/test/GenerateTestProps.targets b/src/Templating/test/GenerateTestProps.targets similarity index 100% rename from src/templating/test/GenerateTestProps.targets rename to src/Templating/test/GenerateTestProps.targets diff --git a/src/templating/test/TemplateTests.props.in b/src/Templating/test/TemplateTests.props.in similarity index 100% rename from src/templating/test/TemplateTests.props.in rename to src/Templating/test/TemplateTests.props.in diff --git a/src/templating/test/Templates.Test/.gitattributes b/src/Templating/test/Templates.Test/.gitattributes similarity index 100% rename from src/templating/test/Templates.Test/.gitattributes rename to src/Templating/test/Templates.Test/.gitattributes diff --git a/src/templating/test/Templates.Test/BaselineTest.cs b/src/Templating/test/Templates.Test/BaselineTest.cs similarity index 100% rename from src/templating/test/Templates.Test/BaselineTest.cs rename to src/Templating/test/Templates.Test/BaselineTest.cs diff --git a/src/templating/test/Templates.Test/ByteOrderMarkTest.cs b/src/Templating/test/Templates.Test/ByteOrderMarkTest.cs similarity index 100% rename from src/templating/test/Templates.Test/ByteOrderMarkTest.cs rename to src/Templating/test/Templates.Test/ByteOrderMarkTest.cs diff --git a/src/templating/test/Templates.Test/CdnScriptTagTests.cs b/src/Templating/test/Templates.Test/CdnScriptTagTests.cs similarity index 100% rename from src/templating/test/Templates.Test/CdnScriptTagTests.cs rename to src/Templating/test/Templates.Test/CdnScriptTagTests.cs diff --git a/src/templating/test/Templates.Test/EmptyWebTemplateTest.cs b/src/Templating/test/Templates.Test/EmptyWebTemplateTest.cs similarity index 100% rename from src/templating/test/Templates.Test/EmptyWebTemplateTest.cs rename to src/Templating/test/Templates.Test/EmptyWebTemplateTest.cs diff --git a/src/templating/test/Templates.Test/Helpers/AddFirewallExclusion.cs b/src/Templating/test/Templates.Test/Helpers/AddFirewallExclusion.cs similarity index 100% rename from src/templating/test/Templates.Test/Helpers/AddFirewallExclusion.cs rename to src/Templating/test/Templates.Test/Helpers/AddFirewallExclusion.cs diff --git a/src/templating/test/Templates.Test/Helpers/AspNetProcess.cs b/src/Templating/test/Templates.Test/Helpers/AspNetProcess.cs similarity index 100% rename from src/templating/test/Templates.Test/Helpers/AspNetProcess.cs rename to src/Templating/test/Templates.Test/Helpers/AspNetProcess.cs diff --git a/src/templating/test/Templates.Test/Helpers/Npm.cs b/src/Templating/test/Templates.Test/Helpers/Npm.cs similarity index 100% rename from src/templating/test/Templates.Test/Helpers/Npm.cs rename to src/Templating/test/Templates.Test/Helpers/Npm.cs diff --git a/src/templating/test/Templates.Test/Helpers/ProcessEx.cs b/src/Templating/test/Templates.Test/Helpers/ProcessEx.cs similarity index 100% rename from src/templating/test/Templates.Test/Helpers/ProcessEx.cs rename to src/Templating/test/Templates.Test/Helpers/ProcessEx.cs diff --git a/src/templating/test/Templates.Test/Helpers/TemplatePackageInstaller.cs b/src/Templating/test/Templates.Test/Helpers/TemplatePackageInstaller.cs similarity index 100% rename from src/templating/test/Templates.Test/Helpers/TemplatePackageInstaller.cs rename to src/Templating/test/Templates.Test/Helpers/TemplatePackageInstaller.cs diff --git a/src/templating/test/Templates.Test/Helpers/TemplateTestBase.cs b/src/Templating/test/Templates.Test/Helpers/TemplateTestBase.cs similarity index 100% rename from src/templating/test/Templates.Test/Helpers/TemplateTestBase.cs rename to src/Templating/test/Templates.Test/Helpers/TemplateTestBase.cs diff --git a/src/templating/test/Templates.Test/Helpers/WebDriverExtensions.cs b/src/Templating/test/Templates.Test/Helpers/WebDriverExtensions.cs similarity index 100% rename from src/templating/test/Templates.Test/Helpers/WebDriverExtensions.cs rename to src/Templating/test/Templates.Test/Helpers/WebDriverExtensions.cs diff --git a/src/templating/test/Templates.Test/Helpers/WebDriverFactory.cs b/src/Templating/test/Templates.Test/Helpers/WebDriverFactory.cs similarity index 100% rename from src/templating/test/Templates.Test/Helpers/WebDriverFactory.cs rename to src/Templating/test/Templates.Test/Helpers/WebDriverFactory.cs diff --git a/src/templating/test/Templates.Test/MvcTemplateTest.cs b/src/Templating/test/Templates.Test/MvcTemplateTest.cs similarity index 100% rename from src/templating/test/Templates.Test/MvcTemplateTest.cs rename to src/Templating/test/Templates.Test/MvcTemplateTest.cs diff --git a/src/templating/test/Templates.Test/RazorPagesTemplateTest.cs b/src/Templating/test/Templates.Test/RazorPagesTemplateTest.cs similarity index 100% rename from src/templating/test/Templates.Test/RazorPagesTemplateTest.cs rename to src/Templating/test/Templates.Test/RazorPagesTemplateTest.cs diff --git a/src/templating/test/Templates.Test/SpaTemplateTest/AngularTemplateTest.cs b/src/Templating/test/Templates.Test/SpaTemplateTest/AngularTemplateTest.cs similarity index 100% rename from src/templating/test/Templates.Test/SpaTemplateTest/AngularTemplateTest.cs rename to src/Templating/test/Templates.Test/SpaTemplateTest/AngularTemplateTest.cs diff --git a/src/templating/test/Templates.Test/SpaTemplateTest/ReactReduxTemplateTest.cs b/src/Templating/test/Templates.Test/SpaTemplateTest/ReactReduxTemplateTest.cs similarity index 100% rename from src/templating/test/Templates.Test/SpaTemplateTest/ReactReduxTemplateTest.cs rename to src/Templating/test/Templates.Test/SpaTemplateTest/ReactReduxTemplateTest.cs diff --git a/src/templating/test/Templates.Test/SpaTemplateTest/ReactTemplateTest.cs b/src/Templating/test/Templates.Test/SpaTemplateTest/ReactTemplateTest.cs similarity index 100% rename from src/templating/test/Templates.Test/SpaTemplateTest/ReactTemplateTest.cs rename to src/Templating/test/Templates.Test/SpaTemplateTest/ReactTemplateTest.cs diff --git a/src/templating/test/Templates.Test/SpaTemplateTest/SpaTemplateTestBase.cs b/src/Templating/test/Templates.Test/SpaTemplateTest/SpaTemplateTestBase.cs similarity index 100% rename from src/templating/test/Templates.Test/SpaTemplateTest/SpaTemplateTestBase.cs rename to src/Templating/test/Templates.Test/SpaTemplateTest/SpaTemplateTestBase.cs diff --git a/src/templating/test/Templates.Test/Templates.Test.csproj b/src/Templating/test/Templates.Test/Templates.Test.csproj similarity index 100% rename from src/templating/test/Templates.Test/Templates.Test.csproj rename to src/Templating/test/Templates.Test/Templates.Test.csproj diff --git a/src/templating/test/Templates.Test/WebApiTemplateTest.cs b/src/Templating/test/Templates.Test/WebApiTemplateTest.cs similarity index 100% rename from src/templating/test/Templates.Test/WebApiTemplateTest.cs rename to src/Templating/test/Templates.Test/WebApiTemplateTest.cs diff --git a/src/templating/test/Templates.Test/template-baselines.json b/src/Templating/test/Templates.Test/template-baselines.json similarity index 100% rename from src/templating/test/Templates.Test/template-baselines.json rename to src/Templating/test/Templates.Test/template-baselines.json diff --git a/src/templating/version.props b/src/Templating/version.props similarity index 100% rename from src/templating/version.props rename to src/Templating/version.props