diff --git a/.azure/pipelines/jobs/default-build.yml b/.azure/pipelines/jobs/default-build.yml index 35074f39e5b4..9aac31c5b31b 100644 --- a/.azure/pipelines/jobs/default-build.yml +++ b/.azure/pipelines/jobs/default-build.yml @@ -53,7 +53,6 @@ parameters: # jobDisplayName: '' - use agentOs by default. artifacts: [] buildDirectory: $(System.DefaultWorkingDirectory)/eng/ - installTar: true installNodeJs: true installJdk: true timeoutInMinutes: 180 @@ -179,9 +178,6 @@ jobs: Write-Host "##vso[task.setvariable variable=SeleniumProcessTrackingFolder]$(Build.SourcesDirectory)\artifacts\tmp\selenium\" ./eng/scripts/InstallGoogleChrome.ps1 displayName: Install Chrome - - ${{ if and(eq(parameters.installTar, 'true'), eq(parameters.agentOs, 'Windows')) }}: - - powershell: ./eng/scripts/InstallTar.ps1 - displayName: Find or install Tar - ${{ if eq(parameters.agentOs, 'Windows') }}: - powershell: Write-Host "##vso[task.prependpath]$(DOTNET_CLI_HOME)\.dotnet\tools" displayName: Add dotnet tools to path diff --git a/.azure/pipelines/richnav.yml b/.azure/pipelines/richnav.yml index 46d8b9e3cb75..e5fe8756d315 100644 --- a/.azure/pipelines/richnav.yml +++ b/.azure/pipelines/richnav.yml @@ -6,59 +6,69 @@ trigger: branches: include: - - blazor-wasm - main - release/* - - internal/release/* + +# Do not run this pipeline for PR validation. +pr: none variables: - name: _BuildArgs value: '/p:SkipTestBuild=true' -- name: Windows86LogArgs +- name: WindowsNonX64LogArgs value: -ExcludeCIBinaryLog stages: - stage: build displayName: Build jobs: - # Build Windows (x64/x86) + # Build Windows (x64/x86/arm64) - template: jobs/default-build.yml parameters: codeSign: false jobName: Windows_build - jobDisplayName: "Build: Windows x64/x86" + jobDisplayName: "Build: Windows x64/x86/arm64" enableRichCodeNavigation: true agentOs: Windows steps: - - script: ./build.cmd + - script: ./eng/build.cmd -ci - -all -arch x64 - /p:EnableRichCodeNavigation=true + -buildNative + /p:EnableRichCodeNavigation=false + $(_BuildArgs) + displayName: Build x64 native assets + + - script: ./eng/build.cmd + -ci + -arch x64 + -all + -noBuildNative + -noBuildRepoTasks $(_BuildArgs) displayName: Build x64 # Build the x86 shared framework # This is going to actually build x86 native assets. - - script: ./build.cmd + - script: ./eng/build.cmd -ci - -noBuildRepoTasks -arch x86 -all -noBuildJava -noBuildNative - /p:EnableRichCodeNavigation=true + -noBuildRepoTasks $(_BuildArgs) - $(Windows86LogArgs) + $(WindowsNonX64LogArgs) displayName: Build x86 - # Windows installers bundle both x86 and x64 assets - - script: ./build.cmd + # Build the arm64 shared framework + - script: ./eng/build.cmd -ci - -noBuildRepoTasks - -buildInstallers + -arch arm64 + -noBuildJava -noBuildNative - /p:AssetManifestFileName=aspnetcore-win-x64-x86.xml - /p:EnableRichCodeNavigation=true + -noBuildRepoTasks $(_BuildArgs) - displayName: Build Installers + $(WindowsNonX64LogArgs) + displayName: Build ARM64 + diff --git a/.editorconfig b/.editorconfig index d7ec0b83dec4..8583569cfc7d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -208,7 +208,7 @@ dotnet_diagnostic.IDE0044.severity = warning dotnet_diagnostic.IDE0073.severity = warning file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. -[**/{test,samples,perf}/**.{cs,vb}] +[{eng/tools/**.cs,**/{test,testassets,samples,Samples,perf,scripts}/**.cs}] # CA1018: Mark attributes with AttributeUsageAttribute dotnet_diagnostic.CA1018.severity = suggestion # CA1507: Use nameof to express symbol names @@ -241,6 +241,10 @@ dotnet_diagnostic.CA1844.severity = suggestion dotnet_diagnostic.CA1845.severity = suggestion # CA1846: Prefer AsSpan over Substring dotnet_diagnostic.CA1846.severity = suggestion +# CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters +dotnet_diagnostic.CA1847.severity = suggestion +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = suggestion # CA2008: Do not create tasks without passing a TaskScheduler dotnet_diagnostic.CA2008.severity = suggestion # CA2012: Use ValueTask correctly diff --git a/Directory.Build.props b/Directory.Build.props index 8f0ac5942284..721f099a7734 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -30,6 +30,9 @@ $(MSBuildProjectName.EndsWith('.Test')) OR $(MSBuildProjectName.EndsWith('.FunctionalTest')) ) ">true true + true true true true + Condition="'$(ExcludeFromSourceBuild)' == '' and + '$(DotNetBuildFromSource)' == 'true' and + '$(IsAspNetCoreApp)' != 'true' and + '$(MSBuildProjectName)' != '$(TargetingPackName)' and + '$(IsAnalyzersProject)' != 'true' and + '$(IsProjectTemplateProject)' != 'true'">true diff --git a/NuGet.config b/NuGet.config index 76ccfd79eeee..06e49041de2f 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,10 +4,10 @@ - + - + @@ -27,10 +27,10 @@ - + - + diff --git a/eng/Baseline.Designer.props b/eng/Baseline.Designer.props index feec59968ea5..7bbef3953446 100644 --- a/eng/Baseline.Designer.props +++ b/eng/Baseline.Designer.props @@ -2,28 +2,28 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - - - + + + @@ -34,120 +34,120 @@ - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - - + + - 6.0.3 + 6.0.4 - - + + - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - + - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 @@ -155,114 +155,114 @@ - 6.0.3 + 6.0.4 - + - + - + - 6.0.3 + 6.0.4 - + - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - + - 6.0.3 + 6.0.4 - - + + - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - - + + - 6.0.3 + 6.0.4 - + - 6.0.3 + 6.0.4 - - - + + + - 6.0.3 + 6.0.4 - - + + - 6.0.3 + 6.0.4 - - + + - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - - + + @@ -270,7 +270,7 @@ - 6.0.3 + 6.0.4 @@ -278,50 +278,50 @@ - 6.0.3 + 6.0.4 - + - + - + - + - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - + - + - + - 6.0.3 + 6.0.4 - - + + @@ -331,8 +331,8 @@ - - + + @@ -340,8 +340,8 @@ - - + + @@ -352,58 +352,58 @@ - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - - + + - 6.0.3 + 6.0.4 - + - + - + - 6.0.3 + 6.0.4 - + - + - + - 6.0.3 + 6.0.4 - + - 6.0.3 + 6.0.4 @@ -411,71 +411,71 @@ - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - + - + - + - + - 6.0.3 + 6.0.4 - - + + - + - - + + - 6.0.3 + 6.0.4 - - + + - 6.0.3 + 6.0.4 - - + + - 6.0.3 + 6.0.4 @@ -491,195 +491,195 @@ - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - + - 6.0.3 + 6.0.4 - - + + - 6.0.3 + 6.0.4 - - + + - 6.0.3 + 6.0.4 - + - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - + - 6.0.3 + 6.0.4 - - + + - - + + - - + + - 6.0.3 + 6.0.4 - - + + - - + + - - + + - - + + - 6.0.3 + 6.0.4 - + - + - + - + - + - 6.0.3 + 6.0.4 - + - + - + - 6.0.3 + 6.0.4 - + - + - + - 6.0.3 + 6.0.4 - + - + - + - 6.0.3 + 6.0.4 - - - - + + + + - 6.0.3 + 6.0.4 @@ -688,69 +688,69 @@ - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - + - 6.0.3 + 6.0.4 - + - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 @@ -769,7 +769,7 @@ - 6.0.3 + 6.0.4 @@ -788,7 +788,7 @@ - 6.0.3 + 6.0.4 @@ -804,46 +804,46 @@ - 6.0.3 + 6.0.4 - + - + - + - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - - - + + + - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 @@ -853,7 +853,7 @@ - 6.0.3 + 6.0.4 @@ -862,73 +862,73 @@ - 6.0.3 + 6.0.4 - + - + - + - 6.0.3 + 6.0.4 - + - + - + - 6.0.3 + 6.0.4 - + - + - + - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 @@ -957,11 +957,11 @@ - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 @@ -979,13 +979,13 @@ - 6.0.3 + 6.0.4 - 6.0.3 + 6.0.4 - + \ No newline at end of file diff --git a/eng/Baseline.xml b/eng/Baseline.xml index 903713249197..e20bb4bb51a8 100644 --- a/eng/Baseline.xml +++ b/eng/Baseline.xml @@ -4,111 +4,111 @@ This file contains a list of all the packages and their versions which were rele Update this list when preparing for a new patch. --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index a15e7e7e1825..67ad201ccdf6 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -9,37 +9,37 @@ --> - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - aca50aef8604cc23910d18edce820e0fa7c61910 + 9b03633bb18b617088b32260065ee385bf9c4491 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - aca50aef8604cc23910d18edce820e0fa7c61910 + 9b03633bb18b617088b32260065ee385bf9c4491 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - aca50aef8604cc23910d18edce820e0fa7c61910 + 9b03633bb18b617088b32260065ee385bf9c4491 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - aca50aef8604cc23910d18edce820e0fa7c61910 + 9b03633bb18b617088b32260065ee385bf9c4491 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - aca50aef8604cc23910d18edce820e0fa7c61910 + 9b03633bb18b617088b32260065ee385bf9c4491 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - aca50aef8604cc23910d18edce820e0fa7c61910 + 9b03633bb18b617088b32260065ee385bf9c4491 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - aca50aef8604cc23910d18edce820e0fa7c61910 + 9b03633bb18b617088b32260065ee385bf9c4491 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - aca50aef8604cc23910d18edce820e0fa7c61910 + 9b03633bb18b617088b32260065ee385bf9c4491 https://github.com/dotnet/runtime @@ -177,9 +177,9 @@ https://github.com/dotnet/runtime 4822e3c3aa77eb82b2fb33c9321f923cf11ddde6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - be98e88c760526452df94ef452fff4602fb5bded + 70ae3df4a6f3c92fb6b315afc405edd10ff38579 https://github.com/dotnet/runtime @@ -193,9 +193,9 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 839cdfb0ecca5e0be3dbccd926e7651ef50fdf10 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 839cdfb0ecca5e0be3dbccd926e7651ef50fdf10 + 70ae3df4a6f3c92fb6b315afc405edd10ff38579 https://github.com/dotnet/runtime @@ -217,9 +217,9 @@ https://github.com/dotnet/runtime 4822e3c3aa77eb82b2fb33c9321f923cf11ddde6 - - https://github.com/dotnet/runtime - 4822e3c3aa77eb82b2fb33c9321f923cf11ddde6 + + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime + 70ae3df4a6f3c92fb6b315afc405edd10ff38579 https://github.com/dotnet/runtime @@ -233,9 +233,9 @@ https://github.com/dotnet/runtime 4822e3c3aa77eb82b2fb33c9321f923cf11ddde6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - be98e88c760526452df94ef452fff4602fb5bded + 70ae3df4a6f3c92fb6b315afc405edd10ff38579 https://github.com/dotnet/runtime @@ -245,33 +245,33 @@ https://github.com/dotnet/runtime 4822e3c3aa77eb82b2fb33c9321f923cf11ddde6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - be98e88c760526452df94ef452fff4602fb5bded + 70ae3df4a6f3c92fb6b315afc405edd10ff38579 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - be98e88c760526452df94ef452fff4602fb5bded + 70ae3df4a6f3c92fb6b315afc405edd10ff38579 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - be98e88c760526452df94ef452fff4602fb5bded + 70ae3df4a6f3c92fb6b315afc405edd10ff38579 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - be98e88c760526452df94ef452fff4602fb5bded + 70ae3df4a6f3c92fb6b315afc405edd10ff38579 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - be98e88c760526452df94ef452fff4602fb5bded + 70ae3df4a6f3c92fb6b315afc405edd10ff38579 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - be98e88c760526452df94ef452fff4602fb5bded + 70ae3df4a6f3c92fb6b315afc405edd10ff38579 @@ -280,22 +280,22 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime be98e88c760526452df94ef452fff4602fb5bded - + https://github.com/dotnet/arcade - 879df783283dfb44c7653493fdf7fd7b07ba6b01 + 1a6b24397e50146d0fece9cfb9c0b87275691e6f - + https://github.com/dotnet/arcade - 879df783283dfb44c7653493fdf7fd7b07ba6b01 + 1a6b24397e50146d0fece9cfb9c0b87275691e6f - + https://github.com/dotnet/arcade - 879df783283dfb44c7653493fdf7fd7b07ba6b01 + 1a6b24397e50146d0fece9cfb9c0b87275691e6f - + https://github.com/dotnet/arcade - 879df783283dfb44c7653493fdf7fd7b07ba6b01 + 1a6b24397e50146d0fece9cfb9c0b87275691e6f diff --git a/eng/Versions.props b/eng/Versions.props index 2d3e47d26e41..2ba62764d9b6 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -8,7 +8,7 @@ 6 0 - 4 + 5 true 6.0.0 - 6.0.4 - 6.0.4 - 6.0.4 - 6.0.4 - 6.0.4 - 6.0.4-servicing.22164.4 + 6.0.5 + 6.0.5 + 6.0.5 + 6.0.5 + 6.0.5 + 6.0.5-servicing.22213.9 6.0.0 6.0.1 6.0.0 @@ -103,36 +103,36 @@ 6.0.0 6.0.0 6.0.0 - 6.0.4-servicing.22164.4 + 6.0.5-servicing.22213.9 6.0.0 6.0.0 6.0.1 - 6.0.2 + 6.0.3 6.0.0 6.0.1 6.0.1 6.0.0 6.0.0 - 6.0.0 + 6.0.1 6.0.0 6.0.0 6.0.0 - 6.0.3 + 6.0.4 6.0.0 6.0.3 - 6.0.4 - 6.0.4 - 6.0.4 - 6.0.4 - 6.0.4 - 6.0.4 - 6.0.4 - 6.0.4 + 6.0.5 + 6.0.5 + 6.0.5 + 6.0.5 + 6.0.5 + 6.0.5 + 6.0.5 + 6.0.5 - 6.0.0-beta.22161.1 - 6.0.0-beta.22161.1 + 6.0.0-beta.22212.5 + 6.0.0-beta.22212.5 2.1.1 2.2.0 - 3.1.23-servicing-22123-12 + 3.1.24-servicing-22180-6 $(MicrosoftAspNetCoreAzureAppServicesSiteExtension31Version) $(MicrosoftAspNetCoreAzureAppServicesSiteExtension31Version) - 5.0.15-servicing-22116-16 + 5.0.16-servicing-22167-6 $(MicrosoftAspNetCoreAzureAppServicesSiteExtension50Version) $(MicrosoftAspNetCoreAzureAppServicesSiteExtension50Version) diff --git a/eng/common/templates/steps/source-build.yml b/eng/common/templates/steps/source-build.yml index ba40dc82f141..abb1b2bcda42 100644 --- a/eng/common/templates/steps/source-build.yml +++ b/eng/common/templates/steps/source-build.yml @@ -43,8 +43,8 @@ steps: # In that case, add variables to allow the download of internal runtimes if the specified versions are not found # in the default public locations. internalRuntimeDownloadArgs= - if [ '$(dotnetclimsrc-read-sas-token-base64)' != '$''(dotnetclimsrc-read-sas-token-base64)' ]; then - internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://dotnetclimsrc.blob.core.windows.net/dotnet /p:DotNetRuntimeSourceFeedKey=$(dotnetclimsrc-read-sas-token-base64) --runtimesourcefeed https://dotnetclimsrc.blob.core.windows.net/dotnet --runtimesourcefeedkey $(dotnetclimsrc-read-sas-token-base64)' + if [ '$(dotnetbuilds-internal-container-read-token-base64)' != '$''(dotnetbuilds-internal-container-read-token-base64)' ]; then + internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://dotnetbuilds.blob.core.windows.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://dotnetbuilds.blob.core.windows.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' fi buildConfig=Release diff --git a/eng/scripts/InstallTar.ps1 b/eng/scripts/InstallTar.ps1 deleted file mode 100644 index 12159a8d0be0..000000000000 --- a/eng/scripts/InstallTar.ps1 +++ /dev/null @@ -1,74 +0,0 @@ -<# -.SYNOPSIS - Finds or installs the Tar command on this system. -.DESCRIPTION - This script searches for Tar on this system. If not found, downloads and extracts Git to use its tar.exe. Prefers - global installation locations even if Git has been downloaded into this repo. -.PARAMETER GitVersion - The version of the Git to install. If not set, the default value is read from global.json. -.PARAMETER Force - Overwrite the existing installation if one exists in this repo and Tar isn't installed globally. -#> -param( - [string]$GitVersion, - [switch]$Force -) - -$ErrorActionPreference = 'Stop' -$ProgressPreference = 'SilentlyContinue' # Workaround PowerShell/PowerShell#2138 - -Set-StrictMode -Version 1 - -# Find tar. If not found, install Git to get it. -$repoRoot = (Join-Path $PSScriptRoot "..\.." -Resolve) -$installDir = "$repoRoot\.tools\Git\win-x64" -$tarCommand = "$installDir\usr\bin\tar.exe" -$finalCommand = "$repoRoot\.tools\tar.exe" - -Write-Host "Windows version and other information..." -cmd.exe /c ver -systeminfo.exe -Write-Host "Processor Architecture: $env:PROCESSOR_ARCHITECTURE" - -Write-Host "Checking $env:SystemRoot\System32\tar.exe" -Get-ChildItem "$env:SystemRoot\System32\ta*.exe" -if (Test-Path "$env:SystemRoot\System32\tar.exe") { - Write-Host "Found $env:SystemRoot\System32\tar.exe" - $tarCommand = "$env:SystemRoot\System32\tar.exe" -} -elseif (Test-Path "$env:ProgramFiles\Git\usr\bin\tar.exe") { - $tarCommand = "$env:ProgramFiles\Git\usr\bin\tar.exe" -} -elseif (Test-Path "${env:ProgramFiles(x86)}\Git\usr\bin\tar.exe") { - $tarCommand = "${env:ProgramFiles(x86)}\Git\usr\bin\tar.exe" -} -elseif (Test-Path "$env:AGENT_HOMEDIRECTORY\externals\git\usr\bin\tar.exe") { - $tarCommand = "$env:AGENT_HOMEDIRECTORY\externals\git\usr\bin\tar.exe" -} -elseif ((Test-Path $tarCommand) -And (-Not $Force)) { - Write-Verbose "Repo-local Git installation and $tarCommand already exist, skipping Git install." -} -else { - if (-not $GitVersion) { - $globalJson = Get-Content "$repoRoot\global.json" | ConvertFrom-Json - $GitVersion = $globalJson.tools.Git - } - - $Uri = "https://netcorenativeassets.blob.core.windows.net/resource-packages/external/windows/git/Git-${GitVersion}-64-bit.zip" - - Import-Module -Name (Join-Path $PSScriptRoot "..\common\native\CommonLibrary.psm1" -Resolve) - $InstallStatus = CommonLibrary\DownloadAndExtract -Uri $Uri -InstallDirectory "$installDir\" -Force:$Force -Verbose - - if ($InstallStatus -Eq $False) { - Write-Error "Installation failed" - exit 1 - } -} - -New-Item "$repoRoot\.tools\" -ErrorAction SilentlyContinue -ItemType Directory -Copy-Item "$tarCommand" "$finalCommand" -Verbose -Write-Host "Tar now available at '$finalCommand'" - -if ($tarCommand -like '*\Git\*') { - $null >.\.tools\tar.fromGit -} diff --git a/global.json b/global.json index 05ed9cd7faca..30a17d2cce7a 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "6.0.103" + "version": "6.0.104" }, "tools": { - "dotnet": "6.0.103", + "dotnet": "6.0.104", "runtimes": { "dotnet/x64": [ "2.1.30", @@ -13,7 +13,7 @@ "$(MicrosoftNETCoreBrowserDebugHostTransportVersion)" ], "aspnetcore/x64": [ - "3.1.23" + "3.1.24" ] }, "Git": "2.22.0", @@ -29,7 +29,7 @@ }, "msbuild-sdks": { "Yarn.MSBuild": "1.22.10", - "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.22161.1", - "Microsoft.DotNet.Helix.Sdk": "6.0.0-beta.22161.1" + "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.22212.5", + "Microsoft.DotNet.Helix.Sdk": "6.0.0-beta.22212.5" } } diff --git a/src/Components/WebAssembly/Authentication.Msal/src/Microsoft.Authentication.WebAssembly.Msal.csproj b/src/Components/WebAssembly/Authentication.Msal/src/Microsoft.Authentication.WebAssembly.Msal.csproj index 8ae5d1c43d7b..1db0368b882c 100644 --- a/src/Components/WebAssembly/Authentication.Msal/src/Microsoft.Authentication.WebAssembly.Msal.csproj +++ b/src/Components/WebAssembly/Authentication.Msal/src/Microsoft.Authentication.WebAssembly.Msal.csproj @@ -1,6 +1,6 @@  - + $(DefaultNetCoreTargetFramework) @@ -25,6 +25,7 @@ $(MSBuildThisFileDirectory)Interop\ + CheckForSourceBuild; CompileInterop; IncludeCompileInteropOutput; $(ResolveStaticWebAssetsInputsDependsOn) @@ -91,5 +92,11 @@ + + + + + + diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj b/src/Components/WebAssembly/WebAssembly.Authentication/src/Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj index 8d6a000d74f0..35c79a73eb8b 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj @@ -1,6 +1,6 @@ - + $(DefaultNetCoreTargetFramework) @@ -26,6 +26,7 @@ $(MSBuildThisFileDirectory)Interop\ + CheckForSourceBuild; CompileInterop; IncludeCompileInteropOutput; $(ResolveStaticWebAssetsInputsDependsOn) @@ -93,4 +94,10 @@ + + + + + + diff --git a/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj b/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj index 07d568ff72e3..4a9dd771989e 100644 --- a/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj +++ b/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj @@ -85,9 +85,8 @@ This package is an internal implementation of the .NET Core SDK and is not meant $(TargetingPackLayoutRoot)$(TargetingPackSubPath) $(LocalDotNetRoot)$(TargetingPackSubPath) - aspnetcore-targeting-pack-$(PackageVersion) - $(InstallersOutputPath)$(ArchiveOutputFileName).zip - $(InstallersOutputPath)$(ArchiveOutputFileName).tar.gz + aspnetcore-targeting-pack-$(PackageVersion)-$(TargetRuntimeIdentifier) + $(InstallersOutputPath)$(ArchiveOutputFileName)$(ArchiveExtension) @@ -221,29 +220,17 @@ This package is an internal implementation of the .NET Core SDK and is not meant - - <_TarCommand>tar - <_TarCommand Condition="Exists('$(RepoRoot).tools\tar.exe')">"$(RepoRoot).tools\tar.exe" - - - <_TarArchiveOutputPath>$(TarArchiveOutputPath) - <_TarArchiveOutputPath - Condition="Exists('$(repoRoot)\.tools\tar.fromGit')">/$(TarArchiveOutputPath.Replace('\','/').Replace(':','')) - - + Outputs="$(ArchiveOutputPath)"> - - - - - - + DestinationFile="$(ArchiveOutputPath)" + Overwrite="true" + Condition="'$(ArchiveExtension)' == '.zip'" /> + + InnerReadFormAsync(CancellationToken cancell else if (HasMultipartFormContentType(contentType)) { var formAccumulator = new KeyValueAccumulator(); + var nonFormOrFileContentDispositionCount = 0; var boundary = GetBoundary(contentType, _options.MultipartBoundaryLengthLimit); var multipartReader = new MultipartReader(boundary, _request.Body) @@ -259,7 +260,11 @@ private async Task InnerReadFormAsync(CancellationToken cancell } else { - System.Diagnostics.Debug.Assert(false, "Unrecognized content-disposition for this section: " + section.ContentDisposition); + if (nonFormOrFileContentDispositionCount++ >= _options.ValueCountLimit) + { + throw new InvalidDataException($"Unrecognized Content-Disposition. Form value count limit {_options.ValueCountLimit} exceeded."); + + } } section = await multipartReader.ReadNextSectionAsync(cancellationToken); diff --git a/src/Http/Http/test/Features/FormFeatureTests.cs b/src/Http/Http/test/Features/FormFeatureTests.cs index 9426ce6dd1b2..67873e223c4d 100644 --- a/src/Http/Http/test/Features/FormFeatureTests.cs +++ b/src/Http/Http/test/Features/FormFeatureTests.cs @@ -165,6 +165,12 @@ private class MockRequestBodyPipeFeature : IRequestBodyPipeFeature InvalidContentDispositionValue + "\r\n" + "\r\n" + +"Foo\r\n"; + + private const string MultipartFormFileNonFormOrFileContentDispositionValue = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + +"Content-Disposition:x" + +"\r\n" + +"\r\n" + "Foo\r\n"; private const string MultipartFormWithField = @@ -468,6 +474,30 @@ public async Task ReadFormAsync_ValueCountLimitExceeded_Throw(bool bufferRequest Assert.Equal("Form value count limit 2 exceeded.", exception.Message); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_NonFormOrFieldContentDisposition_ValueCountLimitExceeded_Throw(bool bufferRequest) + { + var formContent = new List(); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFileNonFormOrFileContentDispositionValue)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFileNonFormOrFileContentDispositionValue)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFileNonFormOrFileContentDispositionValue)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormEnd)); + + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(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(formFeature); + + var exception = await Assert.ThrowsAsync(() => context.Request.ReadFormAsync()); + Assert.Equal("Unrecognized Content-Disposition. Form value count limit 2 exceeded.", exception.Message); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/src/Installers/Debian/TargetingPack/Debian.TargetingPack.debproj b/src/Installers/Debian/TargetingPack/Debian.TargetingPack.debproj index 6966845de3a6..345878cac074 100644 --- a/src/Installers/Debian/TargetingPack/Debian.TargetingPack.debproj +++ b/src/Installers/Debian/TargetingPack/Debian.TargetingPack.debproj @@ -37,7 +37,7 @@ - $(TargetingPackInstallerBaseName)-$(TargetingPackVersion).deb + $(TargetingPackInstallerBaseName)-$(TargetingPackVersion)-$(TargetArchitecture).deb $(TargetDir)$(TargetFileName) $(TargetingPackVersionPrefix) diff --git a/src/Installers/Windows/TargetingPack/TargetingPack.wixproj b/src/Installers/Windows/TargetingPack/TargetingPack.wixproj index 27894df71808..dc20b17662f0 100644 --- a/src/Installers/Windows/TargetingPack/TargetingPack.wixproj +++ b/src/Installers/Windows/TargetingPack/TargetingPack.wixproj @@ -66,7 +66,7 @@ $(InstallersOutputPath) $(TargetingPackVersionPrefix) $(TargetingPackZipVersion)-$(VersionSuffix) - $(TargetingPackHarvestRoot)aspnetcore-targeting-pack-$(TargetingPackZipVersion).zip + $(TargetingPackHarvestRoot)aspnetcore-targeting-pack-$(TargetingPackZipVersion)-$(TargetRuntimeIdentifier).zip diff --git a/src/Installers/Windows/WindowsHostingBundle/DotNetCore.wxs b/src/Installers/Windows/WindowsHostingBundle/DotNetCore.wxs index 25e549b0dedd..3a7095c146a4 100644 --- a/src/Installers/Windows/WindowsHostingBundle/DotNetCore.wxs +++ b/src/Installers/Windows/WindowsHostingBundle/DotNetCore.wxs @@ -1,6 +1,18 @@ + + + + @@ -12,7 +24,7 @@ InstallCondition="VersionNT64 AND (NOT OPT_NO_RUNTIME OR OPT_NO_RUNTIME="0")" InstallCommand="/quiet /norestart" RepairCommand="/quiet /repair" - Permanent="yes" + UninstallCommand="/quiet /uninstall" DetectCondition="DotNetRedistLtsProductVersion_x64 = v$(var.DotNetRedistLtsInstallerProductVersionx64)"> @@ -24,23 +36,9 @@ InstallCondition="(NOT OPT_NO_RUNTIME OR OPT_NO_RUNTIME="0") AND (NOT OPT_NO_X86 OR OPT_NO_X86="0")" InstallCommand="/quiet /norestart" RepairCommand="/quiet /repair" - Permanent="yes" + UninstallCommand="/quiet /uninstall" DetectCondition="DotNetRedistLtsProductVersion_x86 = v$(var.DotNetRedistLtsInstallerProductVersionx86)"> - - - - - - diff --git a/src/Installers/Windows/WindowsHostingBundle/SharedFramework.wxs b/src/Installers/Windows/WindowsHostingBundle/SharedFramework.wxs index c75b5eb36253..33e84d0921f0 100644 --- a/src/Installers/Windows/WindowsHostingBundle/SharedFramework.wxs +++ b/src/Installers/Windows/WindowsHostingBundle/SharedFramework.wxs @@ -1,6 +1,18 @@ + + + + @@ -12,7 +24,7 @@ InstallCondition="VersionNT64 AND (NOT OPT_NO_SHAREDFX OR OPT_NO_SHAREDFX="0")" InstallCommand="/quiet /norestart" RepairCommand="/quiet /repair" - Permanent="yes" + UninstallCommand="/quiet /uninstall" DetectCondition="SharedFxRedistProductVersion_x64 = v$(var.SharedFxInstallerProductVersionx64)"> @@ -24,23 +36,9 @@ InstallCondition="(NOT OPT_NO_SHAREDFX OR OPT_NO_SHAREDFX="0") AND (NOT OPT_NO_X86 OR OPT_NO_X86="0")" InstallCommand="/quiet /norestart" RepairCommand="/quiet /repair" - Permanent="yes" + UninstallCommand="/quiet /uninstall" DetectCondition="SharedFxRedistProductVersion_x86 = v$(var.SharedFxInstallerProductVersionx86)"> - - - - - - diff --git a/src/Mvc/test/Mvc.FunctionalTests/ErrorPageTests.cs b/src/Mvc/test/Mvc.FunctionalTests/ErrorPageTests.cs index 11ff6b17d899..ccfb60a40bb4 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/ErrorPageTests.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/ErrorPageTests.cs @@ -1,14 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.IO; -using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text.Encodings.Web; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation; using Microsoft.AspNetCore.TestHost; @@ -16,7 +12,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; -using Xunit; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Mvc.FunctionalTests @@ -24,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests /// /// Functional test to verify the error reporting of Razor compilation by diagnostic middleware. /// - public class ErrorPageTests : IClassFixture>, IDisposable + public class ErrorPageTests : IClassFixture> { private static readonly string PreserveCompilationContextMessage = HtmlEncoder.Default.Encode( "One or more compilation references may be missing. " + @@ -189,10 +184,5 @@ public async Task AggregateException_FlattensInnerExceptions() Assert.Contains(nullReferenceException, content); Assert.Contains(indexOutOfRangeException, content); } - - public void Dispose() - { - _assemblyTestLog.Dispose(); - } } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/dotnetcli.host.json index 3c34f96f85e3..02abad32e7eb 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/dotnetcli.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/dotnetcli.host.json @@ -85,6 +85,10 @@ "CallsMicrosoftGraph": { "longName": "calls-graph", "shortName": "" + }, + "UseProgramMain": { + "longName": "use-program-main", + "shortName": "" } }, "usageExamples": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/ide.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/ide.host.json index e891e5f2df97..947fd0caa52b 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/ide.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/ide.host.json @@ -43,6 +43,12 @@ "useHttps": true } ], + "symbolInfo": [ + { + "id": "UseProgramMain", + "isVisible": true + } + ], "disableHttpsSymbol": "NoHttps", "supportsDocker": true } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/template.json index 254598a9cfe2..4b203fdd52b6 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/template.json @@ -34,6 +34,21 @@ "wwwroot/**" ], "modifiers": [ + { + "condition": "(!UseProgramMain)", + "exclude": [ + "Program.Main.cs" + ] + }, + { + "condition": "(UseProgramMain)", + "exclude": [ + "Program.cs" + ], + "rename": { + "Program.Main.cs": "Program.cs" + } + }, { "condition": "(!IndividualLocalAuth || UseLocalDB)", "exclude": [ @@ -490,6 +505,13 @@ "datatype": "bool", "description": "If specified, skips the automatic restore of the project on create.", "defaultValue": "false" + }, + "UseProgramMain": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "displayName": "Do not use top-level statements", + "description": "Whether to generate an explicit Program class and Main method instead of top-level statements." } }, "primaryOutputs": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Program.Main.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Program.Main.cs new file mode 100644 index 000000000000..92eb45d80a91 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Program.Main.cs @@ -0,0 +1,169 @@ +#if (OrganizationalAuth || IndividualB2CAuth) +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.UI; +#endif +#if (WindowsAuth) +using Microsoft.AspNetCore.Authentication.Negotiate; +#endif +#if (OrganizationalAuth) +#if (MultiOrgAuth) +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +#endif +using Microsoft.AspNetCore.Authorization; +#endif +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +#if (IndividualLocalAuth) +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI; +#endif +#if (OrganizationalAuth) +using Microsoft.AspNetCore.Mvc.Authorization; +#endif +#if (IndividualLocalAuth) +using Microsoft.EntityFrameworkCore; +#endif +#if (GenerateGraph) +using Graph = Microsoft.Graph; +#endif +#if(MultiOrgAuth) +using Microsoft.IdentityModel.Tokens; +#endif +#if (IndividualLocalAuth) +using BlazorServerWeb_CSharp.Areas.Identity; +#endif +using BlazorServerWeb_CSharp.Data; + +namespace Company.WebApplication1; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + #if (IndividualLocalAuth) + var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + builder.Services.AddDbContext(options => + #if (UseLocalDB) + options.UseSqlServer(connectionString)); + #else + options.UseSqlite(connectionString)); + #endif + builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) + .AddEntityFrameworkStores(); + #elif (OrganizationalAuth) + #if (GenerateApiOrGraph) + var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' '); + + #endif + builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + #if (GenerateApiOrGraph) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) + #if (GenerateApi) + .AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi")) + #endif + #if (GenerateGraph) + .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi")) + #endif + .AddInMemoryTokenCaches(); + #else + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")); + #endif + #elif (IndividualB2CAuth) + #if (GenerateApi) + var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' '); + + #endif + builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + #if (GenerateApi) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAdB2C")) + .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) + .AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi")) + .AddInMemoryTokenCaches(); + #else + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAdB2C")); + #endif + #endif + #if (OrganizationalAuth || IndividualB2CAuth) + builder.Services.AddControllersWithViews() + .AddMicrosoftIdentityUI(); + + builder.Services.AddAuthorization(options => + { + // By default, all incoming requests will be authorized according to the default policy + options.FallbackPolicy = options.DefaultPolicy; + }); + + #elif (WindowsAuth) + builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) + .AddNegotiate(); + + builder.Services.AddAuthorization(options => + { + // By default, all incoming requests will be authorized according to the default policy. + options.FallbackPolicy = options.DefaultPolicy; + }); + + #endif + builder.Services.AddRazorPages(); + #if (OrganizationalAuth || IndividualB2CAuth) + builder.Services.AddServerSideBlazor() + .AddMicrosoftIdentityConsentHandler(); + #else + builder.Services.AddServerSideBlazor(); + #endif + #if (IndividualLocalAuth) + builder.Services.AddScoped>(); + #endif + builder.Services.AddSingleton(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + #if (IndividualLocalAuth) + if (app.Environment.IsDevelopment()) + { + app.UseMigrationsEndPoint(); + } + else + #else + if (!app.Environment.IsDevelopment()) + #endif + { + app.UseExceptionHandler("/Error"); + #if (RequiresHttps) + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + app.UseHttpsRedirection(); + #else + } + + #endif + + app.UseStaticFiles(); + + app.UseRouting(); + + #if (OrganizationalAuth || IndividualAuth || WindowsAuth) + app.UseAuthentication(); + app.UseAuthorization(); + + #endif + #if (OrganizationalAuth || IndividualAuth) + app.MapControllers(); + #endif + app.MapBlazorHub(); + app.MapFallbackToPage("/_Host"); + + app.Run(); + } +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/dotnetcli.host.json index 93b2c5a3bc03..3d2007dc5868 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/dotnetcli.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/dotnetcli.host.json @@ -95,6 +95,10 @@ "CallsMicrosoftGraph": { "longName": "calls-graph", "shortName": "" + }, + "UseProgramMain": { + "longName": "use-program-main", + "shortName": "" } } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/ide.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/ide.host.json index 9005a4d171a3..2ed03203a457 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/ide.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/ide.host.json @@ -47,6 +47,10 @@ "text": "_Progressive Web Application" }, "isVisible": "true" + }, + { + "id": "UseProgramMain", + "isVisible": true } ] } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/template.json index cd44728a4894..9b261b76d232 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/template.json @@ -94,6 +94,24 @@ "Client/wwwroot/icon-512.png" ] }, + { + "condition": "(!UseProgramMain)", + "exclude": [ + "Server/Program.Main.cs", + "Client/Program.Main.cs" + ] + }, + { + "condition": "(UseProgramMain)", + "exclude": [ + "Server/Program.cs", + "Client/Program.cs" + ], + "rename": { + "Server/Program.Main.cs": "Server/Program.cs", + "Client/Program.Main.cs": "Client/Program.cs" + } + }, { "condition": "(!IndividualLocalAuth || UseLocalDB)", "exclude": [ @@ -591,6 +609,13 @@ "GenerateApiOrGraph": { "type": "computed", "value": "(GenerateApi || GenerateGraph)" + }, + "UseProgramMain": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "displayName": "Do not use top-level statements", + "description": "Whether to generate an explicit Program class and Main method instead of top-level statements." } }, "tags": { diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.Main.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.Main.cs new file mode 100644 index 000000000000..8b870e5dc9d3 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.Main.cs @@ -0,0 +1,69 @@ +using Microsoft.AspNetCore.Components.Web; +#if (!NoAuth && Hosted) +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +#endif +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +#if (Hosted) +using ComponentsWebAssembly_CSharp.Client; +#else +using ComponentsWebAssembly_CSharp; +#endif + +namespace Company.WebApplication1; + +public class Program +{ + public static async Task Main(string[] args) + { + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("#app"); + builder.RootComponents.Add("head::after"); + + #if (!Hosted || NoAuth) + builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + #else + builder.Services.AddHttpClient("ComponentsWebAssembly_CSharp.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)) + .AddHttpMessageHandler(); + + // Supply HttpClient instances that include access tokens when making requests to the server project + builder.Services.AddScoped(sp => sp.GetRequiredService().CreateClient("ComponentsWebAssembly_CSharp.ServerAPI")); + #endif + #if(!NoAuth) + + #endif + #if (IndividualLocalAuth) + #if (Hosted) + builder.Services.AddApiAuthorization(); + #else + builder.Services.AddOidcAuthentication(options => + { + #if(MissingAuthority) + // Configure your authentication provider options here. + // For more information, see https://aka.ms/blazor-standalone-auth + #endif + builder.Configuration.Bind("Local", options.ProviderOptions); + }); + #endif + #endif + #if (IndividualB2CAuth) + builder.Services.AddMsalAuthentication(options => + { + builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication); + #if (Hosted) + options.ProviderOptions.DefaultAccessTokenScopes.Add("https://qualified.domain.name/api.id.uri/api-scope"); + #endif + }); + #endif + #if(OrganizationalAuth) + builder.Services.AddMsalAuthentication(options => + { + builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); + #if (Hosted) + options.ProviderOptions.DefaultAccessTokenScopes.Add("api://api.id.uri/api-scope"); + #endif + }); + #endif + + await builder.Build().RunAsync(); + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Program.Main.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Program.Main.cs new file mode 100644 index 000000000000..31835439cd28 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Program.Main.cs @@ -0,0 +1,125 @@ +#if (OrganizationalAuth || IndividualB2CAuth || IndividualLocalAuth) +using Microsoft.AspNetCore.Authentication; +#endif +#if (OrganizationalAuth || IndividualB2CAuth) +using Microsoft.AspNetCore.Authentication.JwtBearer; +#endif +using Microsoft.AspNetCore.ResponseCompression; +#if (IndividualLocalAuth) +using Microsoft.EntityFrameworkCore; +#endif +#if (GenerateGraph) +using Graph = Microsoft.Graph; +#endif +#if (OrganizationalAuth || IndividualB2CAuth) +using Microsoft.Identity.Web; +#endif +#if (IndividualLocalAuth) +using ComponentsWebAssembly_CSharp.Server.Data; +using ComponentsWebAssembly_CSharp.Server.Models; +#endif + +namespace Company.WebApplication1; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + #if (IndividualLocalAuth) + var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + builder.Services.AddDbContext(options => + #if (UseLocalDB) + options.UseSqlServer(connectionString)); + #else + options.UseSqlite(connectionString)); + #endif + builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + + builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) + .AddEntityFrameworkStores(); + + builder.Services.AddIdentityServer() + .AddApiAuthorization(); + + builder.Services.AddAuthentication() + .AddIdentityServerJwt(); + #endif + #if (OrganizationalAuth) + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + #if (GenerateApiOrGraph) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + #if (GenerateApi) + .AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi")) + #endif + #if (GenerateGraph) + .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi")) + #endif + .AddInMemoryTokenCaches(); + #else + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + #endif + #elif (IndividualB2CAuth) + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + #if (GenerateApi) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAdB2C")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi")) + .AddInMemoryTokenCaches(); + #else + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAdB2C")); + #endif + #endif + + builder.Services.AddControllersWithViews(); + builder.Services.AddRazorPages(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + #if (IndividualLocalAuth) + app.UseMigrationsEndPoint(); + #endif + app.UseWebAssemblyDebugging(); + } + else + { + app.UseExceptionHandler("/Error"); + #if (RequiresHttps) + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + #endif + } + + #if (RequiresHttps) + app.UseHttpsRedirection(); + + #endif + app.UseBlazorFrameworkFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + #if (IndividualLocalAuth) + app.UseIdentityServer(); + #endif + #if (OrganizationalAuth || IndividualAuth) + app.UseAuthentication(); + #endif + #if (!NoAuth) + app.UseAuthorization(); + + #endif + + app.MapRazorPages(); + app.MapControllers(); + app.MapFallbackToFile("index.html"); + + app.Run(); + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/dotnetcli.host.json index d97858472b80..6f4f4fb8936d 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/dotnetcli.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/dotnetcli.host.json @@ -27,6 +27,10 @@ "NoHttps": { "longName": "no-https", "shortName": "" + }, + "UseProgramMain": { + "longName": "use-program-main", + "shortName": "" } }, "usageExamples": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/ide.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/ide.host.json index 751a95c5b2ff..f30c4753e2a8 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/ide.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/ide.host.json @@ -20,5 +20,11 @@ "useHttps": true } ], + "symbolInfo": [ + { + "id": "UseProgramMain", + "isVisible": true + } + ], "disableHttpsSymbol": "NoHttps" } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json index 3b7edb411a86..c85130f420f9 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json @@ -29,6 +29,21 @@ "exclude": [ "Properties/launchSettings.json" ] + }, + { + "condition": "(!UseProgramMain)", + "exclude": [ + "Program.Main.cs" + ] + }, + { + "condition": "(UseProgramMain)", + "exclude": [ + "Program.cs" + ], + "rename": { + "Program.Main.cs": "Program.cs" + } } ] } @@ -156,6 +171,13 @@ "datatype": "bool", "defaultValue": "false", "description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth." + }, + "UseProgramMain": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "displayName": "Do not use top-level statements", + "description": "Whether to generate an explicit Program class and Main method instead of top-level statements." } }, "primaryOutputs": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/Program.Main.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/Program.Main.cs new file mode 100644 index 000000000000..6a106499828e --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/Program.Main.cs @@ -0,0 +1,14 @@ +namespace Company.WebApplication1; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + var app = builder.Build(); + + app.MapGet("/", () => "Hello World!"); + + app.Run(); + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/dotnetcli.host.json index 684bc1e734f3..ded3cbf35f23 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/dotnetcli.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/dotnetcli.host.json @@ -14,6 +14,10 @@ "ExcludeLaunchSettings": { "longName": "exclude-launch-settings", "shortName": "" + }, + "UseProgramMain": { + "longName": "use-program-main", + "shortName": "" } }, "usageExamples": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/ide.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/ide.host.json index 2cb41909519c..5c3a869512fd 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/ide.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/ide.host.json @@ -2,5 +2,11 @@ "$schema": "http://json.schemastore.org/vs-2017.3.host", "order": 500, "icon": "ide/gRPC.png", - "supportsDocker": true + "supportsDocker": true, + "symbolInfo": [ + { + "id": "UseProgramMain", + "isVisible": true + } + ] } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/template.json index 2fd14ec9251f..fc4288d87384 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/template.json @@ -26,6 +26,21 @@ "exclude": [ "Properties/launchSettings.json" ] + }, + { + "condition": "(!UseProgramMain)", + "exclude": [ + "Program.Main.cs" + ] + }, + { + "condition": "(UseProgramMain)", + "exclude": [ + "Program.cs" + ], + "rename": { + "Program.Main.cs": "Program.cs" + } } ] } @@ -102,6 +117,13 @@ "fallbackVariableName": "kestrelHttpsPortGenerated" }, "replaces": "5001" + }, + "UseProgramMain": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "displayName": "Do not use top-level statements", + "description": "Whether to generate an explicit Program class and Main method instead of top-level statements." } }, "primaryOutputs": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/Program.Main.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/Program.Main.cs new file mode 100644 index 000000000000..ec1af1a7e9c6 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/Program.Main.cs @@ -0,0 +1,25 @@ +using GrpcService_CSharp.Services; + +namespace Company.WebApplication1; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Additional configuration is required to successfully run gRPC on macOS. + // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 + + // Add services to the container. + builder.Services.AddGrpc(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + app.MapGrpcService(); + app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); + + app.Run(); + } +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/dotnetcli.host.json index 6ce1fbe6eb27..a00ae64f281e 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/dotnetcli.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/dotnetcli.host.json @@ -85,6 +85,10 @@ "CallsMicrosoftGraph": { "longName": "calls-graph", "shortName": "" + }, + "UseProgramMain": { + "longName": "use-program-main", + "shortName": "" } }, "usageExamples": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/ide.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/ide.host.json index f176b8df2a9c..0dc542e09b82 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/ide.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/ide.host.json @@ -45,7 +45,10 @@ } ], "symbolInfo": [ - + { + "id": "UseProgramMain", + "isVisible": true + } ], "disableHttpsSymbol": "NoHttps" } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json index d7226586d37d..989dbb8ab86b 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json @@ -94,6 +94,21 @@ "exclude": [ "Data/SqlServer/**" ] + }, + { + "condition": "(!UseProgramMain)", + "exclude": [ + "Program.Main.cs" + ] + }, + { + "condition": "(UseProgramMain)", + "exclude": [ + "Program.cs" + ], + "rename": { + "Program.Main.cs": "Program.cs" + } } ] } @@ -406,6 +421,13 @@ "GenerateApiOrGraph": { "type": "computed", "value": "(GenerateApi || GenerateGraph)" + }, + "UseProgramMain": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "displayName": "Do not use top-level statements", + "description": "Whether to generate an explicit Program class and Main method instead of top-level statements." } }, "primaryOutputs": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.Main.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.Main.cs new file mode 100644 index 000000000000..3a1a1d68cc06 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.Main.cs @@ -0,0 +1,155 @@ +#if (OrganizationalAuth || IndividualB2CAuth) +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Authorization; +#endif +#if (WindowsAuth) +using Microsoft.AspNetCore.Authentication.Negotiate; +#endif +#if (IndividualLocalAuth) +using Microsoft.AspNetCore.Identity; +#endif +#if (OrganizationalAuth) +using Microsoft.AspNetCore.Mvc.Authorization; +#endif +#if (IndividualLocalAuth) +using Microsoft.EntityFrameworkCore; +#endif +#if (OrganizationalAuth || IndividualB2CAuth) +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.UI; +#endif +#if (MultiOrgAuth) +using Microsoft.IdentityModel.Tokens; +#endif +#if (GenerateGraph) +using Graph = Microsoft.Graph; +#endif +#if (IndividualLocalAuth) +using Company.WebApplication1.Data; +#endif +#if (OrganizationalAuth || IndividualB2CAuth || IndividualLocalAuth || MultiOrgAuth || GenerateGraph || WindowsAuth) + +#endif +namespace Company.WebApplication1; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + #if (IndividualLocalAuth) + var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + builder.Services.AddDbContext(options => + #if (UseLocalDB) + options.UseSqlServer(connectionString)); + #else + options.UseSqlite(connectionString)); + #endif + builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + + builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) + .AddEntityFrameworkStores(); + #elif (OrganizationalAuth) + #if (GenerateApiOrGraph) + var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' '); + + #endif + builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + #if (GenerateApiOrGraph) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) + #if (GenerateApi) + .AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi")) + #endif + #if (GenerateGraph) + .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi")) + #endif + .AddInMemoryTokenCaches(); + #else + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")); + #endif + #elif (IndividualB2CAuth) + #if (GenerateApi) + var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' '); + + #endif + builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + #if (GenerateApi) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAdB2C")) + .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) + .AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi")) + .AddInMemoryTokenCaches(); + #else + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAdB2C")); + #endif + #endif + #if (OrganizationalAuth) + + builder.Services.AddAuthorization(options => + { + // By default, all incoming requests will be authorized according to the default policy. + options.FallbackPolicy = options.DefaultPolicy; + }); + builder.Services.AddRazorPages() + .AddMicrosoftIdentityUI(); + #elif (IndividualB2CAuth) + builder.Services.AddRazorPages() + .AddMicrosoftIdentityUI(); + #elif (WindowsAuth) + + builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) + .AddNegotiate(); + + builder.Services.AddAuthorization(options => + { + // By default, all incoming requests will be authorized according to the default policy. + options.FallbackPolicy = options.DefaultPolicy; + }); + builder.Services.AddRazorPages(); + #else + builder.Services.AddRazorPages(); + #endif + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + #if (IndividualLocalAuth) + if (app.Environment.IsDevelopment()) + { + app.UseMigrationsEndPoint(); + } + else + #else + if (!app.Environment.IsDevelopment()) + #endif + { + app.UseExceptionHandler("/Error"); + #if (RequiresHttps) + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + app.UseHttpsRedirection(); + #else + } + #endif + app.UseStaticFiles(); + + app.UseRouting(); + + #if (OrganizationalAuth || IndividualAuth || WindowsAuth) + app.UseAuthentication(); + #endif + app.UseAuthorization(); + + app.MapRazorPages(); + #if (IndividualB2CAuth || OrganizationalAuth) + app.MapControllers(); + #endif + + app.Run(); + } +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/dotnetcli.host.json index 3c34f96f85e3..02abad32e7eb 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/dotnetcli.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/dotnetcli.host.json @@ -85,6 +85,10 @@ "CallsMicrosoftGraph": { "longName": "calls-graph", "shortName": "" + }, + "UseProgramMain": { + "longName": "use-program-main", + "shortName": "" } }, "usageExamples": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/ide.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/ide.host.json index 12bb6ec5db17..995a75bea9a0 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/ide.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/ide.host.json @@ -45,7 +45,10 @@ } ], "symbolInfo": [ - + { + "id": "UseProgramMain", + "isVisible": true + } ], "disableHttpsSymbol": "NoHttps" } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json index b67fe4f719ef..558a4e818d35 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json @@ -90,6 +90,21 @@ "exclude": [ "Data/SqlServer/**" ] + }, + { + "condition": "(!UseProgramMain)", + "exclude": [ + "Program.Main.cs" + ] + }, + { + "condition": "(UseProgramMain)", + "exclude": [ + "Program.cs" + ], + "rename": { + "Program.Main.cs": "Program.cs" + } } ] } @@ -402,6 +417,13 @@ "GenerateApiOrGraph": { "type": "computed", "value": "(GenerateApi || GenerateGraph)" + }, + "UseProgramMain": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "displayName": "Do not use top-level statements", + "description": "Whether to generate an explicit Program class and Main method instead of top-level statements." } }, "primaryOutputs": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.Main.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.Main.cs new file mode 100644 index 000000000000..5fda9092d06a --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.Main.cs @@ -0,0 +1,159 @@ +#if (OrganizationalAuth || IndividualB2CAuth) +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +#endif +#if (WindowsAuth) +using Microsoft.AspNetCore.Authentication.Negotiate; +#endif +#if (IndividualLocalAuth) +using Microsoft.AspNetCore.Identity; +#endif +#if (OrganizationalAuth) +using Microsoft.AspNetCore.Mvc.Authorization; +#endif +#if (IndividualLocalAuth) +using Microsoft.EntityFrameworkCore; +#endif +#if (OrganizationalAuth || IndividualB2CAuth) +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.UI; +#endif +#if (MultiOrgAuth) +using Microsoft.IdentityModel.Tokens; +#endif +#if (GenerateGraph) +using Graph = Microsoft.Graph; +#endif +#if (IndividualLocalAuth) +using Company.WebApplication1.Data; +#endif +#if (OrganizationalAuth || IndividualB2CAuth || IndividualLocalAuth || MultiOrgAuth || GenerateGraph || WindowsAuth) + +#endif +namespace Company.WebApplication1; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + #if (IndividualLocalAuth) + var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + builder.Services.AddDbContext(options => + #if (UseLocalDB) + options.UseSqlServer(connectionString)); + #else + options.UseSqlite(connectionString)); + #endif + builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + + builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) + .AddEntityFrameworkStores(); + #elif (OrganizationalAuth) + #if (GenerateApiOrGraph) + var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' '); + + #endif + builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + #if (GenerateApiOrGraph) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) + #if (GenerateApi) + .AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi")) + #endif + #if (GenerateGraph) + .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi")) + #endif + .AddInMemoryTokenCaches(); + #else + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")); + #endif + #elif (IndividualB2CAuth) + #if (GenerateApi) + var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' '); + + #endif + builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + #if (GenerateApi) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAdB2C")) + .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) + .AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi")) + .AddInMemoryTokenCaches(); + #else + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAdB2C")); + #endif + #endif + #if (OrganizationalAuth) + + builder.Services.AddControllersWithViews(options => + { + var policy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build(); + options.Filters.Add(new AuthorizeFilter(policy)); + }); + #else + builder.Services.AddControllersWithViews(); + #endif + #if (OrganizationalAuth || IndividualB2CAuth) + builder.Services.AddRazorPages() + .AddMicrosoftIdentityUI(); + #endif + #if (WindowsAuth) + + builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) + .AddNegotiate(); + + builder.Services.AddAuthorization(options => + { + // By default, all incoming requests will be authorized according to the default policy. + options.FallbackPolicy = options.DefaultPolicy; + }); + builder.Services.AddRazorPages(); + #endif + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + #if (IndividualLocalAuth) + if (app.Environment.IsDevelopment()) + { + app.UseMigrationsEndPoint(); + } + else + #else + if (!app.Environment.IsDevelopment()) + #endif + { + app.UseExceptionHandler("/Home/Error"); + #if (RequiresHttps) + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + app.UseHttpsRedirection(); + #else + } + #endif + app.UseStaticFiles(); + + app.UseRouting(); + + #if (OrganizationalAuth || IndividualAuth || WindowsAuth) + app.UseAuthentication(); + #endif + app.UseAuthorization(); + + app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + #if (OrganizationalAuth || IndividualAuth) + app.MapRazorPages(); + #endif + + app.Run(); + } +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/dotnetcli.host.json index 9b97a6182066..79217f16803f 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/dotnetcli.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/dotnetcli.host.json @@ -85,6 +85,10 @@ "DisableOpenAPI": { "longName": "no-openapi", "shortName": "" + }, + "UseProgramMain": { + "longName": "use-program-main", + "shortName": "" } }, "usageExamples": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/ide.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/ide.host.json index c8fdd64b6acd..19355a4eecac 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/ide.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/ide.host.json @@ -55,6 +55,10 @@ "invertBoolean": true, "isVisible": true, "defaultValue": true + }, + { + "id": "UseProgramMain", + "isVisible": true } ], "disableHttpsSymbol": "NoHttps" diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json index ee1f9c886b38..5c9b53aedcd1 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json @@ -37,6 +37,21 @@ "Properties/launchSettings.json" ] }, + { + "condition": "(!UseProgramMain)", + "exclude": [ + "Program.Main.cs" + ] + }, + { + "condition": "(UseProgramMain && !UseMinimalAPIs)", + "exclude": [ + "Program.cs" + ], + "rename": { + "Program.Main.cs": "Program.cs" + } + }, { "condition": "(UseMinimalAPIs)", "exclude": [ @@ -364,6 +379,13 @@ "UseControllers": { "type": "computed", "value": "(!UseMinimalAPIs)" + }, + "UseProgramMain": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "displayName": "Do not use top-level statements", + "description": "Whether to generate an explicit Program class and Main method instead of top-level statements." } }, "primaryOutputs": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.Main.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.Main.cs new file mode 100644 index 000000000000..a0c9ad67e817 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.Main.cs @@ -0,0 +1,95 @@ +#if (OrganizationalAuth || IndividualB2CAuth) +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +#endif +#if (WindowsAuth) +using Microsoft.AspNetCore.Authentication.Negotiate; +#endif +#if (GenerateGraph) +using Graph = Microsoft.Graph; +#endif +#if (OrganizationalAuth || IndividualB2CAuth) +using Microsoft.Identity.Web; +#endif +#if (OrganizationalAuth || IndividualB2CAuth || GenerateGraph || WindowsAuth) + +#endif +namespace Company.WebApplication1; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + #if (OrganizationalAuth) + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + #if (GenerateApiOrGraph) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + #if (GenerateApi) + .AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi")) + #endif + #if (GenerateGraph) + .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi")) + #endif + .AddInMemoryTokenCaches(); + #else + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + #endif + #elif (IndividualB2CAuth) + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + #if (GenerateApi) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAdB2C")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi")) + .AddInMemoryTokenCaches(); + #else + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAdB2C")); + #endif + #endif + + builder.Services.AddControllers(); + #if (EnableOpenAPI) + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + #endif + #if (WindowsAuth) + + builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) + .AddNegotiate(); + + builder.Services.AddAuthorization(options => + { + // By default, all incoming requests will be authorized according to the default policy. + options.FallbackPolicy = options.DefaultPolicy; + }); + #endif + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + #if (EnableOpenAPI) + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + #endif + #if (RequiresHttps) + + app.UseHttpsRedirection(); + #endif + + #if (OrganizationalAuth || IndividualAuth || WindowsAuth) + app.UseAuthentication(); + #endif + app.UseAuthorization(); + + app.MapControllers(); + + app.Run(); + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/dotnetcli.host.json index b1cf98e39bb8..36493a3a4ac7 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/dotnetcli.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/dotnetcli.host.json @@ -11,6 +11,10 @@ "ExcludeLaunchSettings": { "longName": "exclude-launch-settings", "shortName": "" + }, + "UseProgramMain": { + "longName": "use-program-main", + "shortName": "" } }, "usageExamples": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/ide.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/ide.host.json index 13a025d034fa..59f260a583df 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/ide.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/ide.host.json @@ -3,4 +3,10 @@ "order": 300, "icon": "ide/Worker.png", "supportsDocker": true, + "symbolInfo": [ + { + "id": "UseProgramMain", + "isVisible": true + } + ] } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/template.json index 19358bf86a30..d2789afed3ff 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/template.json @@ -30,6 +30,21 @@ "exclude": [ "Properties/launchSettings.json" ] + }, + { + "condition": "(!UseProgramMain)", + "exclude": [ + "Program.Main.cs" + ] + }, + { + "condition": "(UseProgramMain)", + "exclude": [ + "Program.cs" + ], + "rename": { + "Program.Main.cs": "Program.cs" + } } ] } @@ -67,6 +82,13 @@ "datatype": "bool", "description": "If specified, skips the automatic restore of the project on create.", "defaultValue": "false" + }, + "UseProgramMain": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "displayName": "Do not use top-level statements", + "description": "Whether to generate an explicit Program class and Main method instead of top-level statements." } }, "primaryOutputs": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/Program.Main.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/Program.Main.cs new file mode 100644 index 000000000000..d69747f38d51 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/Program.Main.cs @@ -0,0 +1,18 @@ +using Company.Application1; + +namespace Company.WebApplication1; + +public class Program +{ + public static void Main(string[] args) + { + IHost host = Host.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + services.AddHostedService(); + }) + .Build(); + + host.Run(); + } +} \ No newline at end of file diff --git a/src/ProjectTemplates/scripts/Run-AngularProgramMain-Locally.ps1 b/src/ProjectTemplates/scripts/Run-AngularProgramMain-Locally.ps1 new file mode 100644 index 000000000000..93127bb08be0 --- /dev/null +++ b/src/ProjectTemplates/scripts/Run-AngularProgramMain-Locally.ps1 @@ -0,0 +1,12 @@ +#!/usr/bin/env pwsh +#requires -version 4 + +[CmdletBinding(PositionalBinding = $false)] +param() + +Set-StrictMode -Version 2 +$ErrorActionPreference = 'Stop' + +. $PSScriptRoot\Test-Template.ps1 + +Test-Template "angular" "angular --use-program-main" "Microsoft.DotNet.Web.Spa.ProjectTemplates.6.0.6.0.0-dev.nupkg" $false diff --git a/src/ProjectTemplates/scripts/Run-BlazorProgramMain-Locally.ps1 b/src/ProjectTemplates/scripts/Run-BlazorProgramMain-Locally.ps1 new file mode 100644 index 000000000000..3d9fdd64a70d --- /dev/null +++ b/src/ProjectTemplates/scripts/Run-BlazorProgramMain-Locally.ps1 @@ -0,0 +1,13 @@ +#!/usr/bin/env pwsh +#requires -version 4 + +# This script packages, installs and creates a template to help with rapid iteration in the templating area. +[CmdletBinding(PositionalBinding = $false)] +param() + +Set-StrictMode -Version 2 +$ErrorActionPreference = 'Stop' + +. $PSScriptRoot\Test-Template.ps1 + +Test-Template "blazorserver" "blazorserver --use-program-main" "Microsoft.DotNet.Web.ProjectTemplates.6.0.6.0.0-dev.nupkg" $false diff --git a/src/ProjectTemplates/scripts/Run-BlazorWasmProgramMain-Locally.ps1 b/src/ProjectTemplates/scripts/Run-BlazorWasmProgramMain-Locally.ps1 new file mode 100644 index 000000000000..7c8755a8bba3 --- /dev/null +++ b/src/ProjectTemplates/scripts/Run-BlazorWasmProgramMain-Locally.ps1 @@ -0,0 +1,13 @@ +#!/usr/bin/env pwsh +#requires -version 4 + +# This script packages, installs and creates a template to help with rapid iteration in the templating area. +[CmdletBinding(PositionalBinding = $false)] +param() + +Set-StrictMode -Version 2 +$ErrorActionPreference = 'Stop' + +. $PSScriptRoot\Test-Template.ps1 + +Test-Template "blazorwasm" "blazorwasm --use-program-main --hosted --auth Individual" "Microsoft.DotNet.Web.ProjectTemplates.6.0.6.0.0-dev.nupkg" $true diff --git a/src/ProjectTemplates/scripts/Run-EmptyWebProgramMain-Locally.ps1 b/src/ProjectTemplates/scripts/Run-EmptyWebProgramMain-Locally.ps1 new file mode 100644 index 000000000000..7453063baf21 --- /dev/null +++ b/src/ProjectTemplates/scripts/Run-EmptyWebProgramMain-Locally.ps1 @@ -0,0 +1,12 @@ +#!/usr/bin/env pwsh +#requires -version 4 + +[CmdletBinding(PositionalBinding = $false)] +param() + +Set-StrictMode -Version 2 +$ErrorActionPreference = 'Stop' + +. $PSScriptRoot\Test-Template.ps1 + +Test-Template "web" "web --use-program-main" "Microsoft.DotNet.Web.ProjectTemplates.6.0.6.0.0-dev.nupkg" $false diff --git a/src/ProjectTemplates/scripts/Run-RazorProgramMain-Locally.ps1 b/src/ProjectTemplates/scripts/Run-RazorProgramMain-Locally.ps1 new file mode 100644 index 000000000000..4224cf985dd2 --- /dev/null +++ b/src/ProjectTemplates/scripts/Run-RazorProgramMain-Locally.ps1 @@ -0,0 +1,9 @@ +#!/usr/bin/env powershell +#requires -version 4 + +[CmdletBinding(PositionalBinding = $false)] +param() + +. $PSScriptRoot\Test-Template.ps1 + +Test-Template "webapp" "webapp -au Individual --use-program-main" "Microsoft.DotNet.Web.ProjectTemplates.6.0.6.0.0-dev.nupkg" $false diff --git a/src/ProjectTemplates/scripts/Run-ReactProgramMain-Locally.ps1 b/src/ProjectTemplates/scripts/Run-ReactProgramMain-Locally.ps1 new file mode 100644 index 000000000000..df61a5a11740 --- /dev/null +++ b/src/ProjectTemplates/scripts/Run-ReactProgramMain-Locally.ps1 @@ -0,0 +1,12 @@ +#!/usr/bin/env pwsh +#requires -version 4 + +[CmdletBinding(PositionalBinding = $false)] +param() + +Set-StrictMode -Version 2 +$ErrorActionPreference = 'Stop' + +. $PSScriptRoot\Test-Template.ps1 + +Test-Template "react" "react --use-program-main" "Microsoft.DotNet.Web.Spa.ProjectTemplates.6.0.6.0.0-dev.nupkg" $false diff --git a/src/ProjectTemplates/scripts/Run-StarterwebProgramMain-Locally.ps1 b/src/ProjectTemplates/scripts/Run-StarterwebProgramMain-Locally.ps1 new file mode 100644 index 000000000000..076106d3e861 --- /dev/null +++ b/src/ProjectTemplates/scripts/Run-StarterwebProgramMain-Locally.ps1 @@ -0,0 +1,12 @@ +#!/usr/bin/env pwsh +#requires -version 4 + +[CmdletBinding(PositionalBinding = $false)] +param() + +Set-StrictMode -Version 2 +$ErrorActionPreference = 'Stop' + +. $PSScriptRoot\Test-Template.ps1 + +Test-Template "mvc" "mvc -au Individual --use-program-main" "Microsoft.DotNet.Web.ProjectTemplates.6.0.6.0.0-dev.nupkg" $false diff --git a/src/ProjectTemplates/scripts/Run-WebApiProgamMain-Locally.ps1 b/src/ProjectTemplates/scripts/Run-WebApiProgamMain-Locally.ps1 new file mode 100644 index 000000000000..41f794b7eaaf --- /dev/null +++ b/src/ProjectTemplates/scripts/Run-WebApiProgamMain-Locally.ps1 @@ -0,0 +1,12 @@ +#!/usr/bin/env pwsh +#requires -version 4 + +[CmdletBinding(PositionalBinding = $false)] +param() + +Set-StrictMode -Version 2 +$ErrorActionPreference = 'Stop' + +. $PSScriptRoot\Test-Template.ps1 + +Test-Template "webapi" "webapi --use-program-main" "Microsoft.DotNet.Web.ProjectTemplates.6.0.6.0.0-dev.nupkg" $false diff --git a/src/ProjectTemplates/scripts/Run-WorkerProgramMain-Locally.ps1 b/src/ProjectTemplates/scripts/Run-WorkerProgramMain-Locally.ps1 new file mode 100644 index 000000000000..9e0aa3d4607b --- /dev/null +++ b/src/ProjectTemplates/scripts/Run-WorkerProgramMain-Locally.ps1 @@ -0,0 +1,12 @@ +#!/usr/bin/env pwsh +#requires -version 4 + +[CmdletBinding(PositionalBinding = $false)] +param() + +Set-StrictMode -Version 2 +$ErrorActionPreference = 'Stop' + +. $PSScriptRoot\Test-Template.ps1 + +Test-Template "worker" "worker --use-program-main" "Microsoft.DotNet.Web.ProjectTemplates.6.0.6.0.0-dev.nupkg" $false diff --git a/src/ProjectTemplates/scripts/Test-Template.ps1 b/src/ProjectTemplates/scripts/Test-Template.ps1 index 349174ad3d06..05cf0ef7128b 100644 --- a/src/ProjectTemplates/scripts/Test-Template.ps1 +++ b/src/ProjectTemplates/scripts/Test-Template.ps1 @@ -64,7 +64,9 @@ function Test-Template($templateName, $templateArgs, $templateNupkg, $isBlazorWa if ($isBlazorWasmHosted) { Push-Location Server } - dotnet.exe ef migrations add mvc + if ($templateArgs -match '-au') { + dotnet.exe ef migrations add mvc + } dotnet.exe publish --configuration Release Set-Location .\bin\Release\net6.0\publish if ($isBlazorWasm -eq $false) { diff --git a/src/ProjectTemplates/test/BlazorServerTemplateTest.cs b/src/ProjectTemplates/test/BlazorServerTemplateTest.cs index 432c85043bd7..104f2304020d 100644 --- a/src/ProjectTemplates/test/BlazorServerTemplateTest.cs +++ b/src/ProjectTemplates/test/BlazorServerTemplateTest.cs @@ -26,18 +26,26 @@ public BlazorServerTemplateTest(ProjectFactoryFixture projectFactory) [Fact] public Task BlazorServerTemplateWorks_NoAuth() => CreateBuildPublishAsync("blazorservernoauth"); + [Fact] + public Task BlazorServerTemplateWorks_ProgamMainNoAuth() => CreateBuildPublishAsync("blazorservernoauth", args: new [] { "--use-program-main" }); + [Theory] - [InlineData(true)] - [InlineData(false)] + [InlineData(true, null)] + [InlineData(true, new string[] { "--use-program-main" })] + [InlineData(false, null)] + [InlineData(false, new string[] { "--use-program-main" })] [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/30825", Queues = "All.OSX")] - public Task BlazorServerTemplateWorks_IndividualAuth(bool useLocalDB) => CreateBuildPublishAsync("blazorserverindividual" + (useLocalDB ? "uld" : "")); + public Task BlazorServerTemplateWorks_IndividualAuth(bool useLocalDB, string[] args) => CreateBuildPublishAsync("blazorserverindividual" + (useLocalDB ? "uld" : "", args: args)); [Theory] [InlineData("IndividualB2C", null)] [InlineData("IndividualB2C", new string[] { "--called-api-url \"https://graph.microsoft.com\"", "--called-api-scopes user.readwrite" })] + [InlineData("IndividualB2C", new string[] { "--use-program-main", "--called-api-url \"https://graph.microsoft.com\"", "--called-api-scopes user.readwrite" })] [InlineData("SingleOrg", null)] [InlineData("SingleOrg", new string[] { "--called-api-url \"https://graph.microsoft.com\"", "--called-api-scopes user.readwrite" })] + [InlineData("SingleOrg", new string[] { "--use-program-main --called-api-url \"https://graph.microsoft.com\"", "--called-api-scopes user.readwrite" })] [InlineData("SingleOrg", new string[] { "--calls-graph" })] + [InlineData("SingleOrg", new string[] { "--use-program-main --calls-graph" })] public Task BlazorServerTemplate_IdentityWeb_BuildAndPublish(string auth, string[] args) => CreateBuildPublishAsync("blazorserveridweb" + Guid.NewGuid().ToString().Substring(0, 10).ToLowerInvariant(), auth, args); diff --git a/src/ProjectTemplates/test/BlazorWasmTemplateTest.cs b/src/ProjectTemplates/test/BlazorWasmTemplateTest.cs index 97a580dddd8b..dae6148d213a 100644 --- a/src/ProjectTemplates/test/BlazorWasmTemplateTest.cs +++ b/src/ProjectTemplates/test/BlazorWasmTemplateTest.cs @@ -37,6 +37,9 @@ public async Task BlazorWasmStandaloneTemplateCanCreateBuildPublish() [Fact] public Task BlazorWasmHostedTemplateCanCreateBuildPublish() => CreateBuildPublishAsync("blazorhosted", args: new[] { "--hosted" }, serverProject: true); + [Fact] + public Task BlazorWasmHostedTemplateWithProgamMainCanCreateBuildPublish() => CreateBuildPublishAsync("blazorhosted", args: new[] { "--use-program-main", "--hosted" }, serverProject: true); + [Fact] public Task BlazorWasmStandalonePwaTemplateCanCreateBuildPublish() => CreateBuildPublishAsync("blazorstandalonepwa", args: new[] { "--pwa" }); diff --git a/src/ProjectTemplates/test/EmptyWebTemplateTest.cs b/src/ProjectTemplates/test/EmptyWebTemplateTest.cs index e90ccbf5b94c..7468a587b41e 100644 --- a/src/ProjectTemplates/test/EmptyWebTemplateTest.cs +++ b/src/ProjectTemplates/test/EmptyWebTemplateTest.cs @@ -38,17 +38,24 @@ public async Task EmptyWebTemplateCSharp() await EmtpyTemplateCore(languageOverride: null); } + [ConditionalFact] + [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] + public async Task EmptyWebTemplateProgramMainCSharp() + { + await EmtpyTemplateCore(languageOverride: null, args: new [] { "--use-program-main" }); + } + [Fact] public async Task EmptyWebTemplateFSharp() { await EmtpyTemplateCore("F#"); } - private async Task EmtpyTemplateCore(string languageOverride) + private async Task EmtpyTemplateCore(string languageOverride, string[] args = null) { var project = await ProjectFactory.GetOrCreateProject("empty" + (languageOverride == "F#" ? "fsharp" : "csharp"), Output); - var createResult = await project.RunDotNetNewAsync("web", language: languageOverride); + var createResult = await project.RunDotNetNewAsync("web", args: args, language: languageOverride); Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult)); // Avoid the F# compiler. See https://github.com/dotnet/aspnetcore/issues/14022 diff --git a/src/ProjectTemplates/test/GrpcTemplateTest.cs b/src/ProjectTemplates/test/GrpcTemplateTest.cs index 585ac0bfb457..d26d0f8623a6 100644 --- a/src/ProjectTemplates/test/GrpcTemplateTest.cs +++ b/src/ProjectTemplates/test/GrpcTemplateTest.cs @@ -34,14 +34,17 @@ public ITestOutputHelper Output } } - [ConditionalFact] + [ConditionalTheory] [SkipOnHelix("Not supported queues", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] [SkipOnAlpine("https://github.com/grpc/grpc/issues/18338")] - public async Task GrpcTemplate() + [InlineData(true)] + [InlineData(false)] + public async Task GrpcTemplate(bool useProgramMain) { var project = await ProjectFactory.GetOrCreateProject("grpc", Output); - var createResult = await project.RunDotNetNewAsync("grpc"); + var args = useProgramMain ? new [] { "--use-program-main" } : null; + var createResult = await project.RunDotNetNewAsync("grpc", args: args); Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult)); var publishResult = await project.RunDotNetPublishAsync(); diff --git a/src/ProjectTemplates/test/MvcTemplateTest.cs b/src/ProjectTemplates/test/MvcTemplateTest.cs index 47045d6972aa..a5d6bec38066 100644 --- a/src/ProjectTemplates/test/MvcTemplateTest.cs +++ b/src/ProjectTemplates/test/MvcTemplateTest.cs @@ -43,11 +43,15 @@ public ITestOutputHelper Output [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] public async Task MvcTemplate_NoAuthCSharp() => await MvcTemplateCore(languageOverride: null); - private async Task MvcTemplateCore(string languageOverride) + [ConditionalFact] + [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] + public async Task MvcTemplate_ProgramMainNoAuthCSharp() => await MvcTemplateCore(languageOverride: null, new [] { "--use-program-main" }); + + private async Task MvcTemplateCore(string languageOverride, string[] args = null) { var project = await ProjectFactory.GetOrCreateProject("mvcnoauth" + (languageOverride == "F#" ? "fsharp" : "csharp"), Output); - var createResult = await project.RunDotNetNewAsync("mvc", language: languageOverride); + var createResult = await project.RunDotNetNewAsync("mvc", language: languageOverride, args: args); Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult)); var projectExtension = languageOverride == "F#" ? "fsproj" : "csproj"; @@ -75,10 +79,10 @@ private async Task MvcTemplateCore(string languageOverride) Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", project, buildResult)); IEnumerable menuLinks = new List { - PageUrls.HomeUrl, - PageUrls.HomeUrl, - PageUrls.PrivacyFullUrl - }; + PageUrls.HomeUrl, + PageUrls.HomeUrl, + PageUrls.PrivacyFullUrl + }; var footerLinks = new string[] { PageUrls.PrivacyFullUrl }; @@ -116,14 +120,17 @@ private async Task MvcTemplateCore(string languageOverride) } [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] + [InlineData(true, false)] + [InlineData(true, true)] + [InlineData(false, false)] + [InlineData(false, true)] [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] - public async Task MvcTemplate_IndividualAuth(bool useLocalDB) + public async Task MvcTemplate_IndividualAuth(bool useLocalDB, bool useProgramMain) { var project = await ProjectFactory.GetOrCreateProject("mvcindividual" + (useLocalDB ? "uld" : ""), Output); - var createResult = await project.RunDotNetNewAsync("mvc", auth: "Individual", useLocalDB: useLocalDB); + var args = useProgramMain ? new [] { "--use-program-main" } : null; + var createResult = await project.RunDotNetNewAsync("mvc", auth: "Individual", useLocalDB: useLocalDB, args: args); Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult)); var projectFileContents = project.ReadFile($"{project.ProjectName}.csproj"); @@ -148,72 +155,72 @@ public async Task MvcTemplate_IndividualAuth(bool useLocalDB) // Note: if any links are updated here, RazorPagesTemplateTest.cs should be updated as well var pages = new List { - new Page - { - Url = PageUrls.ForgotPassword, - Links = new string [] { - PageUrls.HomeUrl, - PageUrls.HomeUrl, - PageUrls.PrivacyUrl, - PageUrls.RegisterUrl, - PageUrls.LoginUrl, - PageUrls.PrivacyUrl - } - }, - new Page - { - Url = PageUrls.HomeUrl, - Links = new string[] { - PageUrls.HomeUrl, - PageUrls.HomeUrl, - PageUrls.PrivacyUrl, - PageUrls.RegisterUrl, - PageUrls.LoginUrl, - PageUrls.DocsUrl, - PageUrls.PrivacyUrl - } - }, - new Page - { - Url = PageUrls.PrivacyFullUrl, - Links = new string[] { - PageUrls.HomeUrl, - PageUrls.HomeUrl, - PageUrls.PrivacyUrl, - PageUrls.RegisterUrl, - PageUrls.LoginUrl, - PageUrls.PrivacyUrl - } - }, - new Page - { - Url = PageUrls.LoginUrl, - Links = new string[] { - PageUrls.HomeUrl, - PageUrls.HomeUrl, - PageUrls.PrivacyUrl, - PageUrls.RegisterUrl, - PageUrls.LoginUrl, - PageUrls.ForgotPassword, - PageUrls.RegisterUrl, - PageUrls.ResendEmailConfirmation, - PageUrls.ExternalArticle, - PageUrls.PrivacyUrl } - }, - new Page - { - Url = PageUrls.RegisterUrl, - Links = new string [] { - PageUrls.HomeUrl, - PageUrls.HomeUrl, - PageUrls.PrivacyUrl, - PageUrls.RegisterUrl, - PageUrls.LoginUrl, - PageUrls.ExternalArticle, - PageUrls.PrivacyUrl + new Page + { + Url = PageUrls.ForgotPassword, + Links = new string [] { + PageUrls.HomeUrl, + PageUrls.HomeUrl, + PageUrls.PrivacyUrl, + PageUrls.RegisterUrl, + PageUrls.LoginUrl, + PageUrls.PrivacyUrl + } + }, + new Page + { + Url = PageUrls.HomeUrl, + Links = new string[] { + PageUrls.HomeUrl, + PageUrls.HomeUrl, + PageUrls.PrivacyUrl, + PageUrls.RegisterUrl, + PageUrls.LoginUrl, + PageUrls.DocsUrl, + PageUrls.PrivacyUrl + } + }, + new Page + { + Url = PageUrls.PrivacyFullUrl, + Links = new string[] { + PageUrls.HomeUrl, + PageUrls.HomeUrl, + PageUrls.PrivacyUrl, + PageUrls.RegisterUrl, + PageUrls.LoginUrl, + PageUrls.PrivacyUrl + } + }, + new Page + { + Url = PageUrls.LoginUrl, + Links = new string[] { + PageUrls.HomeUrl, + PageUrls.HomeUrl, + PageUrls.PrivacyUrl, + PageUrls.RegisterUrl, + PageUrls.LoginUrl, + PageUrls.ForgotPassword, + PageUrls.RegisterUrl, + PageUrls.ResendEmailConfirmation, + PageUrls.ExternalArticle, + PageUrls.PrivacyUrl } + }, + new Page + { + Url = PageUrls.RegisterUrl, + Links = new string [] { + PageUrls.HomeUrl, + PageUrls.HomeUrl, + PageUrls.PrivacyUrl, + PageUrls.RegisterUrl, + PageUrls.LoginUrl, + PageUrls.ExternalArticle, + PageUrls.PrivacyUrl + } } - } - }; + }; using (var aspNetProcess = project.StartBuiltProjectAsync()) { @@ -234,67 +241,44 @@ public async Task MvcTemplate_IndividualAuth(bool useLocalDB) } } - [ConditionalFact(Skip = "https://github.com/dotnet/aspnetcore/issues/25103")] - [SkipOnHelix("cert failure", Queues = "All.OSX")] + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] // Running these requires the rid-specific runtime pack to be available which is not consistent in all our platform builds. + [SkipOnHelix("cert failure", Queues = "All.OSX;" + HelixConstants.Windows10Arm64)] public async Task MvcTemplate_SingleFileExe() { // This test verifies publishing an MVC app as a single file exe works. We'll limit testing // this to a few operating systems to make our lives easier. - string runtimeIdentifer; - if (OperatingSystem.IsWindows()) - { - runtimeIdentifer = "win-x64"; - } - else if (OperatingSystem.IsLinux()) - { - runtimeIdentifer = "linux-x64"; - } - else - { - return; - } - + var runtimeIdentifer = "win-x64"; var project = await ProjectFactory.GetOrCreateProject("mvcsinglefileexe", Output); project.RuntimeIdentifier = runtimeIdentifer; - var createResult = await project.RunDotNetNewAsync("mvc", auth: "Individual", useLocalDB: true); + var createResult = await project.RunDotNetNewAsync("mvc"); Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult)); var publishResult = await project.RunDotNetPublishAsync(additionalArgs: $"/p:PublishSingleFile=true -r {runtimeIdentifer}", noRestore: false); Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", project, publishResult)); - var pages = new[] + var menuLinks = new[] + { + PageUrls.HomeUrl, + PageUrls.HomeUrl, + PageUrls.PrivacyFullUrl + }; + + var footerLinks = new[] { PageUrls.PrivacyFullUrl }; + + var pages = new List { new Page { - // Verify a view from the app works Url = PageUrls.HomeUrl, - Links = new [] - { - PageUrls.HomeUrl, - PageUrls.RegisterUrl, - PageUrls.LoginUrl, - PageUrls.HomeUrl, - PageUrls.PrivacyUrl, - PageUrls.DocsUrl, - PageUrls.PrivacyUrl - } + Links = menuLinks.Append(PageUrls.DocsUrl).Concat(footerLinks), }, new Page { - // Verify a view from a RCL (in this case IdentityUI) works - Url = PageUrls.RegisterUrl, - Links = new [] - { - PageUrls.HomeUrl, - PageUrls.RegisterUrl, - PageUrls.LoginUrl, - PageUrls.HomeUrl, - PageUrls.PrivacyUrl, - PageUrls.ExternalArticle, - PageUrls.PrivacyUrl - } - }, + Url = PageUrls.PrivacyFullUrl, + Links = menuLinks.Concat(footerLinks), + } }; using var aspNetProcess = project.StartPublishedProjectAsync(usePublishedAppHost: true); diff --git a/src/ProjectTemplates/test/RazorPagesTemplateTest.cs b/src/ProjectTemplates/test/RazorPagesTemplateTest.cs index 1af0a68366ff..3412233f5dd5 100644 --- a/src/ProjectTemplates/test/RazorPagesTemplateTest.cs +++ b/src/ProjectTemplates/test/RazorPagesTemplateTest.cs @@ -34,13 +34,16 @@ public ITestOutputHelper Output } } - [ConditionalFact] + [ConditionalTheory] [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] - public async Task RazorPagesTemplate_NoAuth() + [InlineData(true)] + [InlineData(false)] + public async Task RazorPagesTemplate_NoAuth(bool useProgramMain) { var project = await ProjectFactory.GetOrCreateProject("razorpagesnoauth", Output); - var createResult = await project.RunDotNetNewAsync("razor"); + var args = useProgramMain ? new [] { "--use-program-main" } : null; + var createResult = await project.RunDotNetNewAsync("razor", args: args); Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("razor", project, createResult)); var projectFileContents = ReadFile(project.TemplateOutputDir, $"{project.ProjectName}.csproj"); @@ -104,14 +107,17 @@ public async Task RazorPagesTemplate_NoAuth() } [ConditionalTheory] - [InlineData(false)] - [InlineData(true)] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] - public async Task RazorPagesTemplate_IndividualAuth(bool useLocalDB) + public async Task RazorPagesTemplate_IndividualAuth(bool useLocalDB, bool useProgramMain) { var project = await ProjectFactory.GetOrCreateProject("razorpagesindividual" + (useLocalDB ? "uld" : ""), Output); - var createResult = await project.RunDotNetNewAsync("razor", auth: "Individual", useLocalDB: useLocalDB); + var args = useProgramMain ? new [] { "--use-program-main" } : null; + var createResult = await project.RunDotNetNewAsync("razor", auth: "Individual", useLocalDB: useLocalDB, args: args); Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult)); var projectFileContents = ReadFile(project.TemplateOutputDir, $"{project.ProjectName}.csproj"); @@ -226,12 +232,15 @@ public async Task RazorPagesTemplate_IndividualAuth(bool useLocalDB) [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/28090", Queues = HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] [InlineData("IndividualB2C", null)] [InlineData("IndividualB2C", new string[] { "--called-api-url \"https://graph.microsoft.com\"", "--called-api-scopes user.readwrite" })] + [InlineData("IndividualB2C", new string[] { "--use-program-main --called-api-url \"https://graph.microsoft.com\"", "--called-api-scopes user.readwrite" })] [InlineData("SingleOrg", null)] [InlineData("SingleOrg", new string[] { "--called-api-url \"https://graph.microsoft.com\"", "--called-api-scopes user.readwrite" })] + [InlineData("SingleOrg", new string[] { "--use-program-main --called-api-url \"https://graph.microsoft.com\"", "--called-api-scopes user.readwrite" })] public Task RazorPagesTemplate_IdentityWeb_BuildsAndPublishes(string auth, string[] args) => BuildAndPublishRazorPagesTemplate(auth: auth, args: args); [ConditionalTheory] [InlineData("SingleOrg", new string[] { "--calls-graph" })] + [InlineData("SingleOrg", new string[] { "--use-program-main --calls-graph" })] public Task RazorPagesTemplate_IdentityWeb_BuildsAndPublishes_WithSingleOrg(string auth, string[] args) => BuildAndPublishRazorPagesTemplate(auth: auth, args: args); private async Task BuildAndPublishRazorPagesTemplate(string auth, string[] args) diff --git a/src/ProjectTemplates/test/WebApiTemplateTest.cs b/src/ProjectTemplates/test/WebApiTemplateTest.cs index ddc6e185320e..f727abcc92c0 100644 --- a/src/ProjectTemplates/test/WebApiTemplateTest.cs +++ b/src/ProjectTemplates/test/WebApiTemplateTest.cs @@ -35,10 +35,15 @@ public ITestOutputHelper Output [ConditionalTheory] [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/28090", Queues = HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] [InlineData("IndividualB2C", null)] + [InlineData("IndividualB2C", new string[] { "--use-program-main" })] [InlineData("IndividualB2C", new string[] { "--called-api-url \"https://graph.microsoft.com\"", "--called-api-scopes user.readwrite" })] + [InlineData("IndividualB2C", new string[] { "--use-program-main --called-api-url \"https://graph.microsoft.com\"", "--called-api-scopes user.readwrite" })] [InlineData("SingleOrg", null)] + [InlineData("SingleOrg", new string[] { "--use-program-main" })] [InlineData("SingleOrg", new string[] { "--called-api-url \"https://graph.microsoft.com\"", "--called-api-scopes user.readwrite" })] + [InlineData("SingleOrg", new string[] { "--use-program-main --called-api-url \"https://graph.microsoft.com\"", "--called-api-scopes user.readwrite" })] [InlineData("SingleOrg", new string[] { "--calls-graph" })] + [InlineData("SingleOrg", new string[] { "--use-program-main --calls-graph" })] public Task WebApiTemplateCSharp_IdentityWeb_BuildsAndPublishes(string auth, string[] args) => PublishAndBuildWebApiTemplate(languageOverride: null, auth: auth, args: args); [Fact] @@ -50,11 +55,18 @@ public ITestOutputHelper Output [ConditionalFact] [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] - public async Task WebApiTemplateCSharp_WithoutOpenAPI() + public Task WebApiTemplateProgramMainCSharp() => WebApiTemplateCore(languageOverride: null, args: new [] { "--use-program-main" }); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] + public async Task WebApiTemplateCSharp_WithoutOpenAPI(bool useProgramMain) { var project = await FactoryFixture.GetOrCreateProject("webapinoopenapi", Output); - var createResult = await project.RunDotNetNewAsync("webapi", args: new[] { "--no-openapi" }); + var args = useProgramMain ? new[] { "--use-program-main --no-openapi" } : new[] { "--no-openapi" }; + var createResult = await project.RunDotNetNewAsync("webapi", args: args); Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult)); var buildResult = await project.RunDotNetBuildAsync(); @@ -68,7 +80,7 @@ public async Task WebApiTemplateCSharp_WithoutOpenAPI() await aspNetProcess.AssertNotFound("swagger"); } - private async Task PublishAndBuildWebApiTemplate(string languageOverride, string auth, string[] args) + private async Task PublishAndBuildWebApiTemplate(string languageOverride, string auth, string[] args = null) { var project = await FactoryFixture.GetOrCreateProject("webapi" + (languageOverride == "F#" ? "fsharp" : "csharp") + Guid.NewGuid().ToString().Substring(0, 10).ToLowerInvariant(), Output); @@ -94,9 +106,9 @@ private async Task PublishAndBuildWebApiTemplate(string languageOverrid return project; } - private async Task WebApiTemplateCore(string languageOverride) + private async Task WebApiTemplateCore(string languageOverride, string[] args = null) { - var project = await PublishAndBuildWebApiTemplate(languageOverride, null, null); + var project = await PublishAndBuildWebApiTemplate(languageOverride, null, args); // Avoid the F# compiler. See https://github.com/dotnet/aspnetcore/issues/14022 if (languageOverride != null) diff --git a/src/ProjectTemplates/test/WorkerTemplateTest.cs b/src/ProjectTemplates/test/WorkerTemplateTest.cs index 80e67c18738e..ba82b5073c0e 100644 --- a/src/ProjectTemplates/test/WorkerTemplateTest.cs +++ b/src/ProjectTemplates/test/WorkerTemplateTest.cs @@ -32,16 +32,17 @@ public ITestOutputHelper Output [ConditionalTheory] [OSSkipCondition(OperatingSystems.Linux, SkipReason = "https://github.com/dotnet/sdk/issues/12831")] - [InlineData("C#")] - [InlineData("F#")] + [InlineData("C#", null)] + [InlineData("C#", new string[] { "--use-program-main" })] + [InlineData("F#", null)] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/25404")] - public async Task WorkerTemplateAsync(string language) + public async Task WorkerTemplateAsync(string language, string[] args) { var project = await ProjectFactory.GetOrCreateProject( $"worker-{ language.ToLowerInvariant()[0] }sharp", Output); - var createResult = await project.RunDotNetNewAsync("worker", language: language); + var createResult = await project.RunDotNetNewAsync("worker", language: language, args: args); Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult)); var publishResult = await project.RunDotNetPublishAsync(); diff --git a/src/Security/CookiePolicy/test/CookieChunkingTests.cs b/src/Security/CookiePolicy/test/CookieChunkingTests.cs index 735d7e23d554..5c958fe0abcd 100644 --- a/src/Security/CookiePolicy/test/CookieChunkingTests.cs +++ b/src/Security/CookiePolicy/test/CookieChunkingTests.cs @@ -129,7 +129,7 @@ public void GetLargeChunkedCookieWithMissingChunk_ThrowingDisabled_NotReassemble public void DeleteChunkedCookieWithOptions_AllDeleted() { HttpContext context = new DefaultHttpContext(); - context.Request.Headers.Append("Cookie", "TestCookie=chunks-7"); + context.Request.Headers.Append("Cookie", "TestCookie=chunks-7;TestCookieC1=1;TestCookieC2=2;TestCookieC3=3;TestCookieC4=4;TestCookieC5=5;TestCookieC6=6;TestCookieC7=7"); new ChunkingCookieManager().DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com", Secure = true }); var cookies = context.Response.Headers["Set-Cookie"]; @@ -147,7 +147,40 @@ public void DeleteChunkedCookieWithOptions_AllDeleted() }, cookies); } + [Fact] + public void DeleteChunkedCookieWithMissingRequestCookies_OnlyPresentCookiesDeleted() + { + HttpContext context = new DefaultHttpContext(); + context.Request.Headers.Append("Cookie", "TestCookie=chunks-7;TestCookieC1=1;TestCookieC2=2"); + + new ChunkingCookieManager().DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com", Secure = true }); + var cookies = context.Response.Headers["Set-Cookie"]; + Assert.Equal(3, cookies.Count); + Assert.Equal(new[] + { + "TestCookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure", + "TestCookieC1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure", + "TestCookieC2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure", + }, cookies); + } + [Fact] + public void DeleteChunkedCookieWithMissingRequestCookies_StopsAtMissingChunk() + { + HttpContext context = new DefaultHttpContext(); + // C3 is missing so we don't try to delete C4 either. + context.Request.Headers.Append("Cookie", "TestCookie=chunks-7;TestCookieC1=1;TestCookieC2=2;TestCookieC4=4"); + + new ChunkingCookieManager().DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com", Secure = true }); + var cookies = context.Response.Headers["Set-Cookie"]; + Assert.Equal(3, cookies.Count); + Assert.Equal(new[] + { + "TestCookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure", + "TestCookieC1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure", + "TestCookieC2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure", + }, cookies); + } [Fact] public void DeleteChunkedCookieWithOptionsAndResponseCookies_AllDeleted() diff --git a/src/Servers/HttpSys/HttpSysServer.slnf b/src/Servers/HttpSys/HttpSysServer.slnf index 3990e33925cb..c546d5797a4a 100644 --- a/src/Servers/HttpSys/HttpSysServer.slnf +++ b/src/Servers/HttpSys/HttpSysServer.slnf @@ -4,9 +4,11 @@ "projects": [ "src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj", "src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj", + "src\\FileProviders\\Embedded\\src\\Microsoft.Extensions.FileProviders.Embedded.csproj", "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj", "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj", + "src\\Hosting\\Server.IntegrationTesting\\src\\Microsoft.AspNetCore.Server.IntegrationTesting.csproj", "src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj", "src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj", "src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj", diff --git a/src/Servers/HttpSys/src/DelegationRule.cs b/src/Servers/HttpSys/src/DelegationRule.cs index 1f57f8298558..c454952eea9e 100644 --- a/src/Servers/HttpSys/src/DelegationRule.cs +++ b/src/Servers/HttpSys/src/DelegationRule.cs @@ -13,17 +13,19 @@ namespace Microsoft.AspNetCore.Server.HttpSys public class DelegationRule : IDisposable { private readonly ILogger _logger; - private readonly UrlGroup _urlGroup; private readonly UrlGroup _sourceQueueUrlGroup; private bool _disposed; + /// /// The name of the Http.Sys request queue /// public string QueueName { get; } + /// /// The URL of the Http.Sys Url Prefix /// public string UrlPrefix { get; } + internal RequestQueue Queue { get; } internal DelegationRule(UrlGroup sourceQueueUrlGroup, string queueName, string urlPrefix, ILogger logger) @@ -32,8 +34,7 @@ internal DelegationRule(UrlGroup sourceQueueUrlGroup, string queueName, string u _logger = logger; QueueName = queueName; UrlPrefix = urlPrefix; - Queue = new RequestQueue(queueName, UrlPrefix, _logger, receiver: true); - _urlGroup = Queue.UrlGroup; + Queue = new RequestQueue(queueName, _logger); } /// @@ -51,7 +52,6 @@ public void Dispose() _sourceQueueUrlGroup.UnSetDelegationProperty(Queue, throwOnError: false); } catch (ObjectDisposedException) { /* Server may have been shutdown */ } - _urlGroup.Dispose(); Queue.Dispose(); } } diff --git a/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs b/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs index 8e25a8c21c37..89638a222575 100644 --- a/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs +++ b/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs @@ -19,25 +19,16 @@ internal class RequestQueue private readonly ILogger _logger; private bool _disposed; - internal RequestQueue(string requestQueueName, string urlPrefix, ILogger logger, bool receiver) - : this(urlGroup: null!, requestQueueName, RequestQueueMode.Attach, logger, receiver) + internal RequestQueue(string requestQueueName, ILogger logger) + : this(urlGroup: null, requestQueueName, RequestQueueMode.Attach, logger, receiver: true) { - try - { - UrlGroup = new UrlGroup(this, UrlPrefix.Create(urlPrefix), logger); - } - catch - { - Dispose(); - throw; - } } internal RequestQueue(UrlGroup urlGroup, string? requestQueueName, RequestQueueMode mode, ILogger logger) : this(urlGroup, requestQueueName, mode, logger, false) { } - private RequestQueue(UrlGroup urlGroup, string? requestQueueName, RequestQueueMode mode, ILogger logger, bool receiver) + private RequestQueue(UrlGroup? urlGroup, string? requestQueueName, RequestQueueMode mode, ILogger logger, bool receiver) { _mode = mode; UrlGroup = urlGroup; @@ -117,10 +108,15 @@ private RequestQueue(UrlGroup urlGroup, string? requestQueueName, RequestQueueMo internal SafeHandle Handle { get; } internal ThreadPoolBoundHandle BoundHandle { get; } - internal UrlGroup UrlGroup { get; } + internal UrlGroup? UrlGroup { get; } internal unsafe void AttachToUrlGroup() { + if (UrlGroup == null) + { + throw new NotSupportedException("Can't attach when UrlGroup is null"); + } + Debug.Assert(Created); CheckDisposed(); // Set the association between request queue and url group. After this, requests for registered urls will @@ -138,6 +134,11 @@ internal unsafe void AttachToUrlGroup() internal unsafe void DetachFromUrlGroup() { + if (UrlGroup == null) + { + throw new NotSupportedException("Can't detach when UrlGroup is null"); + } + Debug.Assert(Created); CheckDisposed(); // Break the association between request queue and url group. After this, requests for registered urls diff --git a/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs b/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs index d13264889ddd..59a67ca43195 100644 --- a/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs +++ b/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs @@ -41,24 +41,6 @@ internal unsafe UrlGroup(ServerSession serverSession, ILogger logger) Id = urlGroupId; } - internal unsafe UrlGroup(RequestQueue requestQueue, UrlPrefix url, ILogger logger) - { - _logger = logger; - - ulong urlGroupId = 0; - _created = false; - var statusCode = HttpApi.HttpFindUrlGroupId( - url.FullPrefix, requestQueue.Handle, &urlGroupId); - - if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) - { - throw new HttpSysException((int)statusCode); - } - - Debug.Assert(urlGroupId != 0, "Invalid id returned by HttpCreateUrlGroup"); - Id = urlGroupId; - } - internal ulong Id { get; private set; } internal unsafe void SetMaxConnections(long maxConnections) diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index ffffc050672c..880beebf562d 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -120,7 +120,7 @@ internal Request(RequestContext requestContext) internal ulong RawConnectionId { get; } // No ulongs in public APIs... - public long ConnectionId => (long)RawConnectionId; + public long ConnectionId => RawConnectionId != 0 ? (long)RawConnectionId : (long)UConnectionId; internal ulong RequestId { get; } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index fabae04d35e5..9c45a262dac8 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -289,10 +289,13 @@ internal unsafe void Delegate(DelegationRule destination) PropertyInfoLength = (uint)System.Text.Encoding.Unicode.GetByteCount(destination.UrlPrefix) }; + // Passing 0 for delegateUrlGroupId allows http.sys to find the right group for the + // URL passed in via the property above. If we passed in the receiver's URL group id + // instead of 0, then delegation would fail if the receiver restarted. statusCode = HttpApi.HttpDelegateRequestEx(source.Handle, destination.Queue.Handle, Request.RequestId, - destination.Queue.UrlGroup.Id, + delegateUrlGroupId: 0, propertyInfoSetSize: 1, &property); } diff --git a/src/Servers/HttpSys/src/ServerDelegationPropertyFeature.cs b/src/Servers/HttpSys/src/ServerDelegationPropertyFeature.cs index 14aee50615c9..a97520a837d7 100644 --- a/src/Servers/HttpSys/src/ServerDelegationPropertyFeature.cs +++ b/src/Servers/HttpSys/src/ServerDelegationPropertyFeature.cs @@ -14,18 +14,23 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal class ServerDelegationPropertyFeature : IServerDelegationFeature { private readonly ILogger _logger; - private readonly RequestQueue _queue; + private readonly UrlGroup _urlGroup; public ServerDelegationPropertyFeature(RequestQueue queue, ILogger logger) { - _queue = queue; + if (queue.UrlGroup == null) + { + throw new ArgumentException($"{nameof(queue)}.UrlGroup can't be null"); + } + + _urlGroup = queue.UrlGroup; _logger = logger; } public DelegationRule CreateDelegationRule(string queueName, string uri) { - var rule = new DelegationRule(_queue.UrlGroup, queueName, uri, _logger); - _queue.UrlGroup.SetDelegationProperty(rule.Queue); + var rule = new DelegationRule(_urlGroup, queueName, uri, _logger); + _urlGroup.SetDelegationProperty(rule.Queue); return rule; } } diff --git a/src/Servers/HttpSys/test/FunctionalTests/DelegateTests.cs b/src/Servers/HttpSys/test/FunctionalTests/DelegateTests.cs index ca9dcf3a0736..9e38c7da8b06 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/DelegateTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/DelegateTests.cs @@ -1,13 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.IO; using System.Net.Http; -using System.Threading.Tasks; +using System.Runtime.InteropServices; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.Testing; -using Xunit; namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests { @@ -198,6 +196,72 @@ public async Task UpdateDelegationRuleTest() destination?.Dispose(); } + [ConditionalFact] + [DelegateSupportedCondition(true)] + public async Task DelegateAfterReceiverRestart() + { + var queueName = Guid.NewGuid().ToString(); + using var receiver = Utilities.CreateHttpServer(out var receiverAddress, async httpContext => + { + await httpContext.Response.WriteAsync(_expectedResponseString); + }, + options => + { + options.RequestQueueName = queueName; + }); + + DelegationRule destination = default; + using var delegator = Utilities.CreateHttpServer(out var delegatorAddress, httpContext => + { + var delegateFeature = httpContext.Features.Get(); + delegateFeature.DelegateRequest(destination); + return Task.CompletedTask; + }); + + var delegationProperty = delegator.Features.Get(); + destination = delegationProperty.CreateDelegationRule(queueName, receiverAddress); + + var responseString = await SendRequestAsync(delegatorAddress); + Assert.Equal(_expectedResponseString, responseString); + + // Stop the receiver + receiver?.Dispose(); + + // Start the receiver again but this time we need to attach to the existing queue. + // Due to https://github.com/dotnet/aspnetcore/issues/40359, we have to manually + // register URL prefixes and attach the server's queue to them. + using var receiverRestarted = (MessagePump)Utilities.CreateHttpServer(out receiverAddress, async httpContext => + { + await httpContext.Response.WriteAsync(_expectedResponseString); + }, + options => + { + options.RequestQueueName = queueName; + options.RequestQueueMode = RequestQueueMode.Attach; + options.UrlPrefixes.Clear(); + options.UrlPrefixes.Add(receiverAddress); + }); + AttachToUrlGroup(receiverRestarted.Listener.RequestQueue); + receiverRestarted.Listener.Options.UrlPrefixes.RegisterAllPrefixes(receiverRestarted.Listener.UrlGroup); + + responseString = await SendRequestAsync(delegatorAddress); + Assert.Equal(_expectedResponseString, responseString); + + destination?.Dispose(); + } + + private unsafe void AttachToUrlGroup(RequestQueue requestQueue) + { + var info = new HttpApiTypes.HTTP_BINDING_INFO(); + info.Flags = HttpApiTypes.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT; + info.RequestQueueHandle = requestQueue.Handle.DangerousGetHandle(); + + var infoptr = new IntPtr(&info); + + requestQueue.UrlGroup.SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerBindingProperty, + infoptr, (uint)Marshal.SizeOf()); + } + private async Task SendRequestAsync(string uri) { using var client = new HttpClient(); diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/StartupTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/StartupTests.cs index e0b0ab828131..e6acee1a6d43 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/StartupTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/StartupTests.cs @@ -468,7 +468,6 @@ public async Task RemoveInProcessReference_FailedToFindRequestHandler() [ConditionalFact] [RequiresNewHandler] - [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/40036")] public async Task StartupTimeoutIsApplied() { // From what we can tell, this failure is due to ungraceful shutdown. diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs index 064da12cb634..3c66964cafd4 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs @@ -38,6 +38,14 @@ public HttpParser(bool showErrorDetails) public bool ParseRequestLine(TRequestHandler handler, ref SequenceReader reader) { + // Skip any leading \r or \n on the request line. This is not technically allowed, + // but apparently there are enough clients relying on this that it's worth allowing. + // Peek first as a minor performance optimization; it's a quick inlined check. + if (reader.TryPeek(out byte b) && (b == ByteCR || b == ByteLF)) + { + reader.AdvancePastAny(ByteCR, ByteLF); + } + if (reader.TryReadTo(out ReadOnlySpan requestLine, ByteLF, advancePastDelimiter: true)) { ParseRequestLine(handler, requestLine); diff --git a/src/Servers/Kestrel/Core/test/StartLineTests.cs b/src/Servers/Kestrel/Core/test/StartLineTests.cs index 4c65611e95e4..59fdf6dd0cc1 100644 --- a/src/Servers/Kestrel/Core/test/StartLineTests.cs +++ b/src/Servers/Kestrel/Core/test/StartLineTests.cs @@ -6,6 +6,7 @@ using System.IO.Pipelines; using System.Text; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; @@ -516,6 +517,55 @@ public void AuthorityForms(string rawTarget, string path, string query) DifferentFormsWorkTogether(); } + public static IEnumerable GetCrLfAndMethodCombinations() + { + // HTTP methods to test + var methods = new string[] { + HttpMethods.Connect, + HttpMethods.Delete, + HttpMethods.Get, + HttpMethods.Head, + HttpMethods.Options, + HttpMethods.Patch, + HttpMethods.Post, + HttpMethods.Put, + HttpMethods.Trace + }; + + // Prefixes to test + var crLfPrefixes = new string[] { + "\r", + "\n", + "\r\r\r\r\r", + "\r\n", + "\n\r" + }; + + foreach (var method in methods) + { + foreach (var prefix in crLfPrefixes) + { + yield return new object[] { prefix, method }; + } + } + } + + [Theory] + [MemberData(nameof(GetCrLfAndMethodCombinations))] + public void LeadingCrLfAreAllowed(string startOfRequestLine, string httpMethod) + { + var rawTarget = "http://localhost/path1?q=123&w=xyzw"; + Http1Connection.Reset(); + // RawTarget, Path, QueryString are null after reset + Assert.Null(Http1Connection.RawTarget); + Assert.Null(Http1Connection.Path); + Assert.Null(Http1Connection.QueryString); + + var ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"{startOfRequestLine}{httpMethod} {rawTarget} HTTP/1.1\r\n")); + var reader = new SequenceReader(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); + } + public StartLineTests() { MemoryPool = PinnedBlockMemoryPoolFactory.Create(); diff --git a/src/Shared/ChunkingCookieManager/ChunkingCookieManager.cs b/src/Shared/ChunkingCookieManager/ChunkingCookieManager.cs index 30ec3c66ed63..9a66071ee5d4 100644 --- a/src/Shared/ChunkingCookieManager/ChunkingCookieManager.cs +++ b/src/Shared/ChunkingCookieManager/ChunkingCookieManager.cs @@ -103,7 +103,7 @@ private static int ParseChunksCount(string? value) var chunksCount = ParseChunksCount(value); if (chunksCount > 0) { - var chunks = new string[chunksCount]; + var chunks = new List(10); // chunksCount may be wrong, don't trust it. for (var chunkId = 1; chunkId <= chunksCount; chunkId++) { var chunk = requestCookies[key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture)]; @@ -128,7 +128,7 @@ private static int ParseChunksCount(string? value) return value; } - chunks[chunkId - 1] = chunk; + chunks.Add(chunk); } return string.Join(string.Empty, chunks); @@ -254,13 +254,22 @@ public void DeleteCookie(HttpContext context, string key, CookieOptions options) key + "=" }; - var requestCookie = context.Request.Cookies[key]; - var chunks = ParseChunksCount(requestCookie); + var requestCookies = context.Request.Cookies; + var requestCookie = requestCookies[key]; + long chunks = ParseChunksCount(requestCookie); if (chunks > 0) { for (var i = 1; i <= chunks + 1; i++) { var subkey = key + ChunkKeySuffix + i.ToString(CultureInfo.InvariantCulture); + + // Only delete cookies we received. We received the chunk count cookie so we should have received the others too. + if (string.IsNullOrEmpty(requestCookies[subkey])) + { + chunks = i - 1; + break; + } + keys.Add(subkey + "="); } } diff --git a/src/Testing/src/AssemblyTestLog.cs b/src/Testing/src/AssemblyTestLog.cs index 0e3d06f2eb3e..4e03f7ca3035 100644 --- a/src/Testing/src/AssemblyTestLog.cs +++ b/src/Testing/src/AssemblyTestLog.cs @@ -7,10 +7,8 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; -using System.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog; @@ -22,20 +20,21 @@ namespace Microsoft.AspNetCore.Testing { - public class AssemblyTestLog : IDisposable + public class AssemblyTestLog : IAcceptFailureReports, IDisposable { private const string MaxPathLengthEnvironmentVariableName = "ASPNETCORE_TEST_LOG_MAXPATH"; private const string LogFileExtension = ".log"; private static readonly int MaxPathLength = GetMaxPathLength(); - private static readonly object _lock = new object(); - private static readonly Dictionary _logs = new Dictionary(); + private static readonly object _lock = new(); + private static readonly Dictionary _logs = new(); private readonly ILoggerFactory _globalLoggerFactory; private readonly ILogger _globalLogger; private readonly string _baseDirectory; private readonly Assembly _assembly; private readonly IServiceProvider _serviceProvider; + private bool _testFailureReported; private static int GetMaxPathLength() { @@ -53,6 +52,9 @@ private AssemblyTestLog(ILoggerFactory globalLoggerFactory, ILogger globalLogger _serviceProvider = serviceProvider; } + // internal for testing + internal bool OnCI { get; set; } = SkipOnCIAttribute.OnCI(); + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public IDisposable StartTestLog(ITestOutputHelper output, string className, out ILoggerFactory loggerFactory, [CallerMemberName] string testName = null) => StartTestLog(output, className, out loggerFactory, LogLevel.Debug, testName); @@ -178,11 +180,8 @@ public IServiceProvider CreateLoggerServices(ITestOutputHelper output, string cl return serviceCollection.BuildServiceProvider(); } - // For back compat - public static AssemblyTestLog Create(string assemblyName, string baseDirectory) - => Create(Assembly.Load(new AssemblyName(assemblyName)), baseDirectory); - - public static AssemblyTestLog Create(Assembly assembly, string baseDirectory) + // internal for testing. Expectation is AspNetTestAssembly runner calls ForAssembly() first for every Assembly. + internal static AssemblyTestLog Create(Assembly assembly, string baseDirectory) { var logStart = DateTimeOffset.UtcNow; SerilogLoggerProvider serilogLoggerProvider = null; @@ -224,26 +223,46 @@ public static AssemblyTestLog ForAssembly(Assembly assembly) { if (!_logs.TryGetValue(assembly, out var log)) { - var baseDirectory = TestFileOutputContext.GetOutputDirectory(assembly); + var stackTrace = Environment.StackTrace; + if (!stackTrace.Contains( + "Microsoft.AspNetCore.Testing" +#if NETCOREAPP + , StringComparison.Ordinal +#endif + )) + { + throw new InvalidOperationException($"Unexpected initial {nameof(ForAssembly)} caller."); + } - log = Create(assembly, baseDirectory); - _logs[assembly] = log; + var baseDirectory = TestFileOutputContext.GetOutputDirectory(assembly); - // Try to clear previous logs, continue if it fails. + // Try to clear previous logs, continue if it fails. Do this before creating new global logger. var assemblyBaseDirectory = TestFileOutputContext.GetAssemblyBaseDirectory(assembly); - if (!string.IsNullOrEmpty(assemblyBaseDirectory) && !TestFileOutputContext.GetPreserveExistingLogsInOutput(assembly)) + if (!string.IsNullOrEmpty(assemblyBaseDirectory) && + !TestFileOutputContext.GetPreserveExistingLogsInOutput(assembly)) { try { Directory.Delete(assemblyBaseDirectory, recursive: true); } - catch { } + catch + { + } } + + log = Create(assembly, baseDirectory); + _logs[assembly] = log; } + return log; } } + public void ReportTestFailure() + { + _testFailureReported = true; + } + private static TestFrameworkFileLoggerAttribute GetFileLoggerAttribute(Assembly assembly) => assembly.GetCustomAttribute() ?? throw new InvalidOperationException($"No {nameof(TestFrameworkFileLoggerAttribute)} found on the assembly {assembly.GetName().Name}. " @@ -269,13 +288,32 @@ private static SerilogLoggerProvider ConfigureFileLogging(string fileName, DateT .MinimumLevel.Verbose() .WriteTo.File(fileName, outputTemplate: "[{TimestampOffset}] [{SourceContext}] [{Level}] {Message:l}{NewLine}{Exception}", flushToDiskInterval: TimeSpan.FromSeconds(1), shared: true) .CreateLogger(); + return new SerilogLoggerProvider(serilogger, dispose: true); } - public void Dispose() + void IDisposable.Dispose() { (_serviceProvider as IDisposable)?.Dispose(); _globalLoggerFactory.Dispose(); + + // Clean up if no tests failed and we're not running local tests. (Ignoring tests of this class, OnCI is + // true on both build and Helix agents.) In particular, remove the directory containing the global.log + // file. All test class log files for this assembly are in subdirectories of this location. + if (!_testFailureReported && + OnCI && + _baseDirectory is not null && + Directory.Exists(_baseDirectory)) + { + try + { + Directory.Delete(_baseDirectory, recursive: true); + } + catch + { + // Best effort. Ignore problems deleting locked logged files. + } + } } private class AssemblyLogTimestampOffsetEnricher : ILogEventEnricher diff --git a/src/Testing/src/AssemblyTestLogFixtureAttribute.cs b/src/Testing/src/AssemblyTestLogFixtureAttribute.cs new file mode 100644 index 000000000000..e4a4452cd458 --- /dev/null +++ b/src/Testing/src/AssemblyTestLogFixtureAttribute.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Testing; + +public class AssemblyTestLogFixtureAttribute : AssemblyFixtureAttribute +{ + public AssemblyTestLogFixtureAttribute() : base(typeof(AssemblyTestLog)) + { + } +} diff --git a/src/Testing/src/build/Microsoft.AspNetCore.Testing.props b/src/Testing/src/build/Microsoft.AspNetCore.Testing.props index 063e9094d172..47d06dfef7a7 100644 --- a/src/Testing/src/build/Microsoft.AspNetCore.Testing.props +++ b/src/Testing/src/build/Microsoft.AspNetCore.Testing.props @@ -11,8 +11,8 @@ + BeforeTargets="GetAssemblyAttributes" + Condition="'$(GenerateLoggingTestingAssemblyAttributes)' != 'false'"> true false @@ -24,6 +24,7 @@ <_Parameter2>Microsoft.AspNetCore.Testing + <_Parameter1>$(PreserveExistingLogsInOutput) <_Parameter2>$(TargetFramework) diff --git a/src/Testing/src/xunit/AspNetTestAssemblyRunner.cs b/src/Testing/src/xunit/AspNetTestAssemblyRunner.cs index a83446375ddd..1d71bdf939be 100644 --- a/src/Testing/src/xunit/AspNetTestAssemblyRunner.cs +++ b/src/Testing/src/xunit/AspNetTestAssemblyRunner.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.Testing { public class AspNetTestAssemblyRunner : XunitTestAssemblyRunner { - private readonly Dictionary _assemblyFixtureMappings = new Dictionary(); + private readonly Dictionary _assemblyFixtureMappings = new(); public AspNetTestAssemblyRunner( ITestAssembly testAssembly, @@ -26,6 +27,9 @@ public AspNetTestAssemblyRunner( { } + // internal for testing + internal IEnumerable Fixtures => _assemblyFixtureMappings.Values; + protected override async Task AfterTestAssemblyStartingAsync() { await base.AfterTestAssemblyStartingAsync(); @@ -33,8 +37,8 @@ protected override async Task AfterTestAssemblyStartingAsync() // Find all the AssemblyFixtureAttributes on the test assembly await Aggregator.RunAsync(async () => { - var fixturesAttributes = ((IReflectionAssemblyInfo)TestAssembly.Assembly) - .Assembly + var assembly = ((IReflectionAssemblyInfo)TestAssembly.Assembly).Assembly; + var fixturesAttributes = assembly .GetCustomAttributes(typeof(AssemblyFixtureAttribute), false) .Cast() .ToList(); @@ -42,15 +46,30 @@ await Aggregator.RunAsync(async () => // Instantiate all the fixtures foreach (var fixtureAttribute in fixturesAttributes) { - var ctorWithDiagnostics = fixtureAttribute.FixtureType.GetConstructor(new[] { typeof(IMessageSink) }); object instance = null; - if (ctorWithDiagnostics != null) + var staticCreator = fixtureAttribute.FixtureType.GetMethod( + name: "ForAssembly", + bindingAttr: BindingFlags.Public | BindingFlags.Static, + binder: null, + types: new[] { typeof(Assembly) }, + modifiers: null); + if (staticCreator is null) { - instance = Activator.CreateInstance(fixtureAttribute.FixtureType, DiagnosticMessageSink); + var ctorWithDiagnostics = fixtureAttribute + .FixtureType + .GetConstructor(new[] { typeof(IMessageSink) }); + if (ctorWithDiagnostics is null) + { + instance = Activator.CreateInstance(fixtureAttribute.FixtureType); + } + else + { + instance = Activator.CreateInstance(fixtureAttribute.FixtureType, DiagnosticMessageSink); + } } else { - instance = Activator.CreateInstance(fixtureAttribute.FixtureType); + instance = staticCreator.Invoke(obj: null, parameters: new[] { assembly }); } _assemblyFixtureMappings[fixtureAttribute.FixtureType] = instance; @@ -66,12 +85,12 @@ await Aggregator.RunAsync(async () => protected override async Task BeforeTestAssemblyFinishedAsync() { // Dispose fixtures - foreach (var disposable in _assemblyFixtureMappings.Values.OfType()) + foreach (var disposable in Fixtures.OfType()) { Aggregator.Run(disposable.Dispose); } - foreach (var disposable in _assemblyFixtureMappings.Values.OfType()) + foreach (var disposable in Fixtures.OfType()) { await Aggregator.RunAsync(disposable.DisposeAsync); } @@ -79,12 +98,13 @@ protected override async Task BeforeTestAssemblyFinishedAsync() await base.BeforeTestAssemblyFinishedAsync(); } - protected override Task RunTestCollectionAsync( + protected override async Task RunTestCollectionAsync( IMessageBus messageBus, ITestCollection testCollection, IEnumerable testCases, CancellationTokenSource cancellationTokenSource) - => new AspNetTestCollectionRunner( + { + var runSummary = await new AspNetTestCollectionRunner( _assemblyFixtureMappings, testCollection, testCases, @@ -92,6 +112,17 @@ protected override Task RunTestCollectionAsync( messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), - cancellationTokenSource).RunAsync(); + cancellationTokenSource) + .RunAsync(); + if (runSummary.Failed != 0) + { + foreach (var fixture in Fixtures.OfType()) + { + fixture.ReportTestFailure(); + } + } + + return runSummary; + } } } diff --git a/src/Testing/src/xunit/IAcceptFailureReports.cs b/src/Testing/src/xunit/IAcceptFailureReports.cs new file mode 100644 index 000000000000..30ca366b3f1b --- /dev/null +++ b/src/Testing/src/xunit/IAcceptFailureReports.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Testing; + +internal interface IAcceptFailureReports +{ + void ReportTestFailure(); +} diff --git a/src/Testing/test/AspNetTestAssemblyRunnerTest.cs b/src/Testing/test/AspNetTestAssemblyRunnerTest.cs new file mode 100644 index 000000000000..dbce4c69bb82 --- /dev/null +++ b/src/Testing/test/AspNetTestAssemblyRunnerTest.cs @@ -0,0 +1,219 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Testing; + +public class AspNetTestAssemblyRunnerTest +{ + private const int NotCalled = -1; + + [Fact] + public async Task ForAssemblyHasHigherPriorityThanConstructors() + { + var runner = TestableAspNetTestAssemblyRunner.Create(typeof(TestAssemblyFixtureWithAll)); + + await runner.AfterTestAssemblyStartingAsync_Public(); + + Assert.NotNull(runner.Fixtures); + var fixtureObject = Assert.Single(runner.Fixtures); + var fixture = Assert.IsType(fixtureObject); + Assert.False(fixture.ConstructorWithMessageSinkCalled); + Assert.True(fixture.ForAssemblyCalled); + Assert.False(fixture.ParameterlessConstructorCalled); + } + + [Fact] + public async Task ConstructorWithMessageSinkHasHigherPriorityThanParameterlessConstructor() + { + var runner = TestableAspNetTestAssemblyRunner.Create(typeof(TestAssemblyFixtureWithMessageSink)); + + await runner.AfterTestAssemblyStartingAsync_Public(); + + Assert.NotNull(runner.Fixtures); + var fixtureObject = Assert.Single(runner.Fixtures); + var fixture = Assert.IsType(fixtureObject); + Assert.True(fixture.ConstructorWithMessageSinkCalled); + Assert.False(fixture.ParameterlessConstructorCalled); + } + + [Fact] + public async Task CalledInExpectedOrder_SuccessWithDispose() + { + var runner = TestableAspNetTestAssemblyRunner.Create(typeof(TextAssemblyFixtureWithDispose)); + + var runSummary = await runner.RunAsync(); + + Assert.NotNull(runSummary); + Assert.Equal(0, runSummary.Failed); + Assert.Equal(0, runSummary.Skipped); + Assert.Equal(1, runSummary.Total); + + Assert.NotNull(runner.Fixtures); + var fixtureObject = Assert.Single(runner.Fixtures); + var fixture = Assert.IsType(fixtureObject); + Assert.Equal(NotCalled, fixture.ReportTestFailureCalledAt); + Assert.Equal(0, fixture.DisposeCalledAt); + } + + [Fact] + public async Task CalledInExpectedOrder_FailedWithDispose() + { + var runner = TestableAspNetTestAssemblyRunner.Create( + typeof(TextAssemblyFixtureWithDispose), + failTestCase: true); + + var runSummary = await runner.RunAsync(); + + Assert.NotNull(runSummary); + Assert.Equal(1, runSummary.Failed); + Assert.Equal(0, runSummary.Skipped); + Assert.Equal(1, runSummary.Total); + + Assert.NotNull(runner.Fixtures); + var fixtureObject = Assert.Single(runner.Fixtures); + var fixture = Assert.IsType(fixtureObject); + Assert.Equal(0, fixture.ReportTestFailureCalledAt); + Assert.Equal(1, fixture.DisposeCalledAt); + } + + [Fact] + public async Task CalledInExpectedOrder_SuccessWithAsyncDispose() + { + var runner = TestableAspNetTestAssemblyRunner.Create(typeof(TestAssemblyFixtureWithAsyncDispose)); + + var runSummary = await runner.RunAsync(); + + Assert.NotNull(runSummary); + Assert.Equal(0, runSummary.Failed); + Assert.Equal(0, runSummary.Skipped); + Assert.Equal(1, runSummary.Total); + + Assert.NotNull(runner.Fixtures); + var fixtureObject = Assert.Single(runner.Fixtures); + var fixture = Assert.IsType(fixtureObject); + Assert.Equal(0, fixture.InitializeAsyncCalledAt); + Assert.Equal(NotCalled, fixture.ReportTestFailureCalledAt); + Assert.Equal(1, fixture.AsyncDisposeCalledAt); + } + + [Fact] + public async Task CalledInExpectedOrder_FailedWithAsyncDispose() + { + var runner = TestableAspNetTestAssemblyRunner.Create( + typeof(TestAssemblyFixtureWithAsyncDispose), + failTestCase: true); + + var runSummary = await runner.RunAsync(); + + Assert.NotNull(runSummary); + Assert.Equal(1, runSummary.Failed); + Assert.Equal(0, runSummary.Skipped); + Assert.Equal(1, runSummary.Total); + + Assert.NotNull(runner.Fixtures); + var fixtureObject = Assert.Single(runner.Fixtures); + var fixture = Assert.IsType(fixtureObject); + Assert.Equal(0, fixture.InitializeAsyncCalledAt); + Assert.Equal(1, fixture.ReportTestFailureCalledAt); + Assert.Equal(2, fixture.AsyncDisposeCalledAt); + } + + private class TestAssemblyFixtureWithAll + { + private TestAssemblyFixtureWithAll(bool forAssemblyCalled) + { + ForAssemblyCalled = forAssemblyCalled; + } + + public TestAssemblyFixtureWithAll() + { + ParameterlessConstructorCalled = true; + } + + public TestAssemblyFixtureWithAll(IMessageSink messageSink) + { + ConstructorWithMessageSinkCalled = true; + } + + public static TestAssemblyFixtureWithAll ForAssembly(Assembly assembly) + { + return new TestAssemblyFixtureWithAll(forAssemblyCalled: true); + } + + public bool ParameterlessConstructorCalled { get; } + + public bool ConstructorWithMessageSinkCalled { get; } + + public bool ForAssemblyCalled { get; } + } + + private class TestAssemblyFixtureWithMessageSink + { + public TestAssemblyFixtureWithMessageSink() + { + ParameterlessConstructorCalled = true; + } + + public TestAssemblyFixtureWithMessageSink(IMessageSink messageSink) + { + ConstructorWithMessageSinkCalled = true; + } + + public bool ParameterlessConstructorCalled { get; } + + public bool ConstructorWithMessageSinkCalled { get; } + } + + private class TextAssemblyFixtureWithDispose : IAcceptFailureReports, IDisposable + { + private int _position; + + public int ReportTestFailureCalledAt { get; private set; } = NotCalled; + + public int DisposeCalledAt { get; private set; } = NotCalled; + + void IAcceptFailureReports.ReportTestFailure() + { + ReportTestFailureCalledAt = _position++; + } + + void IDisposable.Dispose() + { + DisposeCalledAt = _position++; + } + } + + private class TestAssemblyFixtureWithAsyncDispose : IAcceptFailureReports, IAsyncLifetime + { + private int _position; + + public int InitializeAsyncCalledAt { get; private set; } = NotCalled; + + public int ReportTestFailureCalledAt { get; private set; } = NotCalled; + + public int AsyncDisposeCalledAt { get; private set; } = NotCalled; + + Task IAsyncLifetime.InitializeAsync() + { + InitializeAsyncCalledAt = _position++; + return Task.CompletedTask; + } + + void IAcceptFailureReports.ReportTestFailure() + { + ReportTestFailureCalledAt = _position++; + } + + Task IAsyncLifetime.DisposeAsync() + { + AsyncDisposeCalledAt = _position++; + return Task.CompletedTask; + } + } +} diff --git a/src/Testing/test/AssemblyTestLogTests.cs b/src/Testing/test/AssemblyTestLogTests.cs index 57c3b87b1681..bd3bbb1b8e29 100644 --- a/src/Testing/test/AssemblyTestLogTests.cs +++ b/src/Testing/test/AssemblyTestLogTests.cs @@ -4,21 +4,17 @@ using System; using System.IO; using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using System.Threading.Tasks; -using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing.Tests; using Xunit; -namespace Microsoft.Extensions.Logging.Testing.Tests +namespace Microsoft.AspNetCore.Testing { public class AssemblyTestLogTests : LoggedTest { - private static readonly Assembly ThisAssembly = typeof(AssemblyTestLogTests).GetTypeInfo().Assembly; - private static readonly string ThisAssemblyName = ThisAssembly.GetName().Name; - private static readonly string TFM = ThisAssembly.GetCustomAttributes().OfType().FirstOrDefault().TargetFramework; - [Fact] public void FunctionalLogs_LogsPreservedFromNonQuarantinedTest() { @@ -35,15 +31,52 @@ public void FunctionalLogs_LogsPreservedFromQuarantinedTest() public void ForAssembly_ReturnsSameInstanceForSameAssembly() { Assert.Same( - AssemblyTestLog.ForAssembly(ThisAssembly), - AssemblyTestLog.ForAssembly(ThisAssembly)); + AssemblyTestLog.ForAssembly(TestableAssembly.ThisAssembly), + AssemblyTestLog.ForAssembly(TestableAssembly.ThisAssembly)); } + [Fact] + public Task ForAssemblyWritesToAssemblyBaseDirectory() => + RunTestLogFunctionalTest((tempDir) => + { + var logger = LoggerFactory.CreateLogger("Test"); + + var assembly = TestableAssembly.Create(typeof(AssemblyTestLog), logDirectory: tempDir); + var assemblyName = assembly.GetName().Name; + var testName = $"{TestableAssembly.TestClassName}.{TestableAssembly.TestMethodName}"; + + var tfmPath = Path.Combine(tempDir, assemblyName, TestableAssembly.TFM); + var globalLogPath = Path.Combine(tfmPath, "global.log"); + var testLog = Path.Combine(tfmPath, TestableAssembly.TestClassName, $"{testName}.log"); + + using var testAssemblyLog = AssemblyTestLog.ForAssembly(assembly); + testAssemblyLog.OnCI = true; + logger.LogInformation("Created test log in {baseDirectory}", tempDir); + + using (testAssemblyLog.StartTestLog( + output: null, + className: $"{assemblyName}.{TestableAssembly.TestClassName}", + loggerFactory: out var testLoggerFactory, + minLogLevel: LogLevel.Trace, + testName: testName)) + { + var testLogger = testLoggerFactory.CreateLogger("TestLogger"); + testLogger.LogInformation("Information!"); + testLogger.LogTrace("Trace!"); + } + + Assert.True(File.Exists(globalLogPath), $"Expected global log file {globalLogPath} to exist."); + Assert.True(File.Exists(testLog), $"Expected test log file {testLog} to exist."); + + logger.LogInformation("Finished test log in {baseDirectory}", tempDir); + }); + [Fact] public void TestLogWritesToITestOutputHelper() { var output = new TestTestOutputHelper(); - var assemblyLog = AssemblyTestLog.Create(ThisAssemblyName, baseDirectory: null); + + using var assemblyLog = AssemblyTestLog.Create(TestableAssembly.ThisAssembly, baseDirectory: null); using (assemblyLog.StartTestLog(output, "NonExistant.Test.Class", out var loggerFactory)) { @@ -69,11 +102,19 @@ public Task TestLogEscapesIllegalFileNames() => { var illegalTestName = "T:e/s//t"; var escapedTestName = "T_e_s_t"; - using (var testAssemblyLog = AssemblyTestLog.Create(ThisAssemblyName, baseDirectory: tempDir)) - using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, resolvedTestName: out var resolvedTestname, out var _, testName: illegalTestName)) - { - Assert.Equal(escapedTestName, resolvedTestname); - } + + using var testAssemblyLog = AssemblyTestLog.Create( + TestableAssembly.ThisAssembly, + baseDirectory: tempDir); + using var disposable = testAssemblyLog.StartTestLog( + output: null, + className: "FakeTestAssembly.FakeTestClass", + loggerFactory: out var testLoggerFactory, + minLogLevel: LogLevel.Trace, + resolvedTestName: out var resolvedTestname, + out var _, + testName: illegalTestName); + Assert.Equal(escapedTestName, resolvedTestname); }); [Fact] @@ -84,11 +125,19 @@ public Task TestLogWritesToGlobalLogFile() => // but it's also testing the test logging facility. So this is pretty meta ;) var logger = LoggerFactory.CreateLogger("Test"); - using (var testAssemblyLog = AssemblyTestLog.Create(ThisAssemblyName, tempDir)) + using (var testAssemblyLog = AssemblyTestLog.Create( + TestableAssembly.ThisAssembly, + baseDirectory: tempDir)) { + testAssemblyLog.OnCI = false; logger.LogInformation("Created test log in {baseDirectory}", tempDir); - using (testAssemblyLog.StartTestLog(output: null, className: $"{ThisAssemblyName}.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: "FakeTestName")) + using (testAssemblyLog.StartTestLog( + output: null, + className: $"{TestableAssembly.ThisAssemblyName}.FakeTestClass", + loggerFactory: out var testLoggerFactory, + minLogLevel: LogLevel.Trace, + testName: "FakeTestName")) { var testLogger = testLoggerFactory.CreateLogger("TestLogger"); testLogger.LogInformation("Information!"); @@ -98,8 +147,17 @@ public Task TestLogWritesToGlobalLogFile() => logger.LogInformation("Finished test log in {baseDirectory}", tempDir); - var globalLogPath = Path.Combine(tempDir, ThisAssemblyName, TFM, "global.log"); - var testLog = Path.Combine(tempDir, ThisAssemblyName, TFM, "FakeTestClass", "FakeTestName.log"); + var globalLogPath = Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "global.log"); + var testLog = Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "FakeTestClass", + "FakeTestName.log"); Assert.True(File.Exists(globalLogPath), $"Expected global log file {globalLogPath} to exist"); Assert.True(File.Exists(testLog), $"Expected test log file {testLog} to exist"); @@ -120,31 +178,139 @@ [OFFSET] [TestLifetime] [Information] Finished test FakeTestName in DURATION ", testLogContent, ignoreLineEndingDifferences: true); }); + [Fact] + public Task TestLogCleansLogFiles_AfterSuccessfulRun() => + RunTestLogFunctionalTest((tempDir) => + { + var logger = LoggerFactory.CreateLogger("Test"); + var globalLogPath = Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "global.log"); + var testLog = Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "FakeTestClass", + "FakeTestName.log"); + + using (var testAssemblyLog = AssemblyTestLog.Create( + TestableAssembly.ThisAssembly, + baseDirectory: tempDir)) + { + testAssemblyLog.OnCI = true; + logger.LogInformation("Created test log in {baseDirectory}", tempDir); + + using (testAssemblyLog.StartTestLog( + output: null, + className: $"{TestableAssembly.ThisAssemblyName}.FakeTestClass", + loggerFactory: out var testLoggerFactory, + minLogLevel: LogLevel.Trace, + testName: "FakeTestName")) + { + var testLogger = testLoggerFactory.CreateLogger("TestLogger"); + testLogger.LogInformation("Information!"); + testLogger.LogTrace("Trace!"); + } + + Assert.True(File.Exists(globalLogPath), $"Expected global log file {globalLogPath} to exist."); + Assert.True(File.Exists(testLog), $"Expected test log file {testLog} to exist."); + } + + logger.LogInformation("Finished test log in {baseDirectory}", tempDir); + + Assert.True(!File.Exists(globalLogPath), $"Expected no global log file {globalLogPath} to exist."); + Assert.True(!File.Exists(testLog), $"Expected no test log file {testLog} to exist."); + }); + + [Fact] + public Task TestLogDoesNotCleanLogFiles_AfterFailedRun() => + RunTestLogFunctionalTest((tempDir) => + { + var logger = LoggerFactory.CreateLogger("Test"); + var globalLogPath = Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "global.log"); + var testLog = Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "FakeTestClass", + "FakeTestName.log"); + + using (var testAssemblyLog = AssemblyTestLog.Create( + TestableAssembly.ThisAssembly, + baseDirectory: tempDir)) + { + testAssemblyLog.OnCI = true; + logger.LogInformation("Created test log in {baseDirectory}", tempDir); + + using (testAssemblyLog.StartTestLog( + output: null, + className: $"{TestableAssembly.ThisAssemblyName}.FakeTestClass", + loggerFactory: out var testLoggerFactory, + minLogLevel: LogLevel.Trace, + testName: "FakeTestName")) + { + var testLogger = testLoggerFactory.CreateLogger("TestLogger"); + testLogger.LogInformation("Information!"); + testLogger.LogTrace("Trace!"); + } + + testAssemblyLog.ReportTestFailure(); + } + + logger.LogInformation("Finished test log in {baseDirectory}", tempDir); + + Assert.True(File.Exists(globalLogPath), $"Expected global log file {globalLogPath} to exist."); + Assert.True(File.Exists(testLog), $"Expected test log file {testLog} to exist."); + }); + [Fact] public Task TestLogTruncatesTestNameToAvoidLongPaths() => RunTestLogFunctionalTest((tempDir) => { - var longTestName = new string('0', 50) + new string('1', 50) + new string('2', 50) + new string('3', 50) + new string('4', 50); + var longTestName = new string('0', 50) + new string('1', 50) + new string('2', 50) + + new string('3', 50) + new string('4', 50); var logger = LoggerFactory.CreateLogger("Test"); - using (var testAssemblyLog = AssemblyTestLog.Create(ThisAssemblyName, tempDir)) + using (var testAssemblyLog = AssemblyTestLog.Create( + TestableAssembly.ThisAssembly, + baseDirectory: tempDir)) { + testAssemblyLog.OnCI = false; logger.LogInformation("Created test log in {baseDirectory}", tempDir); - using (testAssemblyLog.StartTestLog(output: null, className: $"{ThisAssemblyName}.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: longTestName)) + using (testAssemblyLog.StartTestLog( + output: null, + className: $"{TestableAssembly.ThisAssemblyName}.FakeTestClass", + loggerFactory: out var testLoggerFactory, + minLogLevel: LogLevel.Trace, + testName: longTestName)) { testLoggerFactory.CreateLogger("TestLogger").LogInformation("Information!"); } } + logger.LogInformation("Finished test log in {baseDirectory}", tempDir); - var testLogFiles = new DirectoryInfo(Path.Combine(tempDir, ThisAssemblyName, TFM, "FakeTestClass")).EnumerateFiles(); + var testLogFiles = new DirectoryInfo( + Path.Combine(tempDir, TestableAssembly.ThisAssemblyName, TestableAssembly.TFM, "FakeTestClass")) + .EnumerateFiles(); var testLog = Assert.Single(testLogFiles); var testFileName = Path.GetFileNameWithoutExtension(testLog.Name); // The first half of the file comes from the beginning of the test name passed to the logger - Assert.Equal(longTestName.Substring(0, testFileName.Length / 2), testFileName.Substring(0, testFileName.Length / 2)); + Assert.Equal( + longTestName.Substring(0, testFileName.Length / 2), + testFileName.Substring(0, testFileName.Length / 2)); + // The last half of the file comes from the ending of the test name passed to the logger - Assert.Equal(longTestName.Substring(longTestName.Length - testFileName.Length / 2, testFileName.Length / 2), testFileName.Substring(testFileName.Length - testFileName.Length / 2, testFileName.Length / 2)); + Assert.Equal( + longTestName.Substring(longTestName.Length - testFileName.Length / 2, testFileName.Length / 2), + testFileName.Substring(testFileName.Length - testFileName.Length / 2, testFileName.Length / 2)); }); [Fact] @@ -152,27 +318,46 @@ public Task TestLogEnumerateFilenamesToAvoidCollisions() => RunTestLogFunctionalTest((tempDir) => { var logger = LoggerFactory.CreateLogger("Test"); - using (var testAssemblyLog = AssemblyTestLog.Create(ThisAssemblyName, tempDir)) + using (var testAssemblyLog = AssemblyTestLog.Create( + TestableAssembly.ThisAssembly, + baseDirectory: tempDir)) { + testAssemblyLog.OnCI = false; logger.LogInformation("Created test log in {baseDirectory}", tempDir); for (var i = 0; i < 10; i++) { - using (testAssemblyLog.StartTestLog(output: null, className: $"{ThisAssemblyName}.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: "FakeTestName")) + using (testAssemblyLog.StartTestLog( + output: null, + className: $"{TestableAssembly.ThisAssemblyName}.FakeTestClass", + loggerFactory: out var testLoggerFactory, + minLogLevel: LogLevel.Trace, + testName: "FakeTestName")) { testLoggerFactory.CreateLogger("TestLogger").LogInformation("Information!"); } } } + logger.LogInformation("Finished test log in {baseDirectory}", tempDir); // The first log file exists - Assert.True(File.Exists(Path.Combine(tempDir, ThisAssemblyName, TFM, "FakeTestClass", "FakeTestName.log"))); + Assert.True(File.Exists(Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "FakeTestClass", + "FakeTestName.log"))); // Subsequent files exist for (var i = 0; i < 9; i++) { - Assert.True(File.Exists(Path.Combine(tempDir, ThisAssemblyName, TFM, "FakeTestClass", $"FakeTestName.{i}.log"))); + Assert.True(File.Exists(Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "FakeTestClass", + $"FakeTestName.{i}.log"))); } }); diff --git a/src/Testing/test/TestableAspNetTestAssemblyRunner.cs b/src/Testing/test/TestableAspNetTestAssemblyRunner.cs new file mode 100644 index 000000000000..17f9373b34c2 --- /dev/null +++ b/src/Testing/test/TestableAspNetTestAssemblyRunner.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Moq; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Testing; + +public class TestableAspNetTestAssemblyRunner : AspNetTestAssemblyRunner +{ + private TestableAspNetTestAssemblyRunner( + ITestAssembly testAssembly, + IEnumerable testCases, + IMessageSink diagnosticMessageSink, + IMessageSink executionMessageSink, + ITestFrameworkExecutionOptions executionOptions) : base( + testAssembly, + testCases, + diagnosticMessageSink, + executionMessageSink, + executionOptions) + { + } + + public static TestableAspNetTestAssemblyRunner Create(Type fixtureType, bool failTestCase = false) + { + var assembly = TestableAssembly.Create(fixtureType, failTestCase: failTestCase); + var testAssembly = GetTestAssembly(assembly); + var testCase = GetTestCase(assembly, testAssembly); + + return new TestableAspNetTestAssemblyRunner( + testAssembly, + new[] { testCase }, + diagnosticMessageSink: new NullMessageSink(), + executionMessageSink: new NullMessageSink(), + executionOptions: Mock.Of()); + + // Do not call Xunit.Sdk.Reflector.Wrap(assembly) because it uses GetTypes() and that method + // throws NotSupportedException for a dynamic assembly. + IAssemblyInfo GetAssemblyInfo(Assembly assembly) + { + var testAssemblyName = assembly.GetName().Name; + var assemblyInfo = new Mock(); + assemblyInfo.SetupGet(r => r.Assembly).Returns(assembly); + assemblyInfo.SetupGet(r => r.Name).Returns(testAssemblyName); + assemblyInfo + .SetupGet(r => r.AssemblyPath) + .Returns(Path.Combine(Directory.GetCurrentDirectory(), $"{testAssemblyName}.dll")); + + foreach (var attribute in CustomAttributeData.GetCustomAttributes(assembly)) + { + var attributeInfo = Reflector.Wrap(attribute); + var attributeName = attribute.AttributeType.AssemblyQualifiedName; + assemblyInfo + .Setup(r => r.GetCustomAttributes(attributeName)) + .Returns(new[] { attributeInfo }); + } + + var typeInfo = Reflector.Wrap(assembly.GetType(TestableAssembly.TestClassName)); + assemblyInfo.Setup(r => r.GetType(TestableAssembly.TestClassName)).Returns(typeInfo); + assemblyInfo.Setup(r => r.GetTypes(It.IsAny())).Returns(new[] { typeInfo }); + + return assemblyInfo.Object; + } + + ITestAssembly GetTestAssembly(Assembly assembly) + { + var assemblyInfo = GetAssemblyInfo(assembly); + + return new TestAssembly(assemblyInfo); + } + + IXunitTestCase GetTestCase(Assembly assembly, ITestAssembly testAssembly) + { + var testAssemblyName = assembly.GetName().Name; + var testCollection = new TestCollection( + testAssembly, + collectionDefinition: null, + displayName: $"Mock collection for '{testAssemblyName}'."); + + var type = assembly.GetType(TestableAssembly.TestClassName); + var testClass = new TestClass(testCollection, Reflector.Wrap(type)); + var method = type.GetMethod(TestableAssembly.TestMethodName); + var methodInfo = Reflector.Wrap(method); + var testMethod = new TestMethod(testClass, methodInfo); + + return new XunitTestCase( + diagnosticMessageSink: new NullMessageSink(), + defaultMethodDisplay: TestMethodDisplay.ClassAndMethod, + defaultMethodDisplayOptions: TestMethodDisplayOptions.None, + testMethod: testMethod); + } + } + + public Task AfterTestAssemblyStartingAsync_Public() + { + return base.AfterTestAssemblyStartingAsync(); + } +} diff --git a/src/Testing/test/TestableAssembly.cs b/src/Testing/test/TestableAssembly.cs new file mode 100644 index 000000000000..fd4b557cca12 --- /dev/null +++ b/src/Testing/test/TestableAssembly.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using Xunit; + +namespace Microsoft.AspNetCore.Testing; + +/* Creates a very simple dynamic assembly containing + * + * [Assembly: TestFramework( + * typeName: "Microsoft.AspNetCore.Testing.AspNetTestFramework", + * assemblyName: "Microsoft.AspNetCore.Testing")] + * [assembly: AssemblyFixture(typeof({fixtureType}))] + * [assembly: TestOutputDirectory( + * preserveExistingLogsInOutput: "false", + * targetFramework: TFM, + * baseDirectory: logDirectory)] // logdirectory is passed into Create(...). + * + * public class MyTestClass + * { + * public MyTestClass() { } + * + * [Fact] + * public MyTestMethod() + * { + * if (failTestCase) // Not exactly; condition checked during generation. + * { + * Assert.True(condition: false); + * } + * } + * } + */ +public static class TestableAssembly +{ + public static readonly Assembly ThisAssembly = typeof(TestableAssembly).GetTypeInfo().Assembly; + public static readonly string ThisAssemblyName = ThisAssembly.GetName().Name; + + private static readonly TestOutputDirectoryAttribute ThisOutputDirectoryAttribute = + ThisAssembly.GetCustomAttributes().OfType().FirstOrDefault(); + public static readonly string BaseDirectory = ThisOutputDirectoryAttribute.BaseDirectory; + public static readonly string TFM = ThisOutputDirectoryAttribute.TargetFramework; + + public const string TestClassName = "MyTestClass"; + public const string TestMethodName = "MyTestMethod"; + + public static Assembly Create(Type fixtureType, string logDirectory = null, bool failTestCase = false) + { + var frameworkConstructor = typeof(TestFrameworkAttribute) + .GetConstructor(new[] { typeof(string), typeof(string) }); + var frameworkBuilder = new CustomAttributeBuilder( + frameworkConstructor, + new[] { "Microsoft.AspNetCore.Testing.AspNetTestFramework", "Microsoft.AspNetCore.Testing" }); + + var fixtureConstructor = typeof(AssemblyFixtureAttribute).GetConstructor(new[] { typeof(Type) }); + var fixtureBuilder = new CustomAttributeBuilder(fixtureConstructor, new[] { fixtureType }); + + var outputConstructor = typeof(TestOutputDirectoryAttribute).GetConstructor( + new[] { typeof(string), typeof(string), typeof(string) }); + var outputBuilder = new CustomAttributeBuilder(outputConstructor, new[] { "false", TFM, logDirectory }); + + var testAssemblyName = $"Test{Guid.NewGuid():n}"; + var assemblyName = new AssemblyName(testAssemblyName); + var assembly = AssemblyBuilder.DefineDynamicAssembly( + assemblyName, + AssemblyBuilderAccess.Run, + new[] { frameworkBuilder, fixtureBuilder, outputBuilder }); + + var module = assembly.DefineDynamicModule(testAssemblyName); + var type = module.DefineType(TestClassName, TypeAttributes.Public); + type.DefineDefaultConstructor(MethodAttributes.Public); + + var method = type.DefineMethod(TestMethodName, MethodAttributes.Public); + var factConstructor = typeof(FactAttribute).GetConstructor(Array.Empty()); + var factBuilder = new CustomAttributeBuilder(factConstructor, Array.Empty()); + method.SetCustomAttribute(factBuilder); + + var generator = method.GetILGenerator(); + if (failTestCase) + { + // Assert.True(condition: false); + generator.Emit(OpCodes.Ldc_I4_0); + var trueInfo = typeof(Assert).GetMethod("True", new[] { typeof(bool) }); + generator.EmitCall(OpCodes.Call, trueInfo, optionalParameterTypes: null); + } + + generator.Emit(OpCodes.Ret); + type.CreateType(); + + return assembly; + } +} diff --git a/src/submodules/googletest b/src/submodules/googletest index c9461a9b55ba..af29db7ec28d 160000 --- a/src/submodules/googletest +++ b/src/submodules/googletest @@ -1 +1 @@ -Subproject commit c9461a9b55ba954df0489bab6420eb297bed846b +Subproject commit af29db7ec28d6df1c7f0f745186884091e602e07 diff --git a/src/submodules/spa-templates b/src/submodules/spa-templates index 3d16ec44cf4b..08a4e743ccec 160000 --- a/src/submodules/spa-templates +++ b/src/submodules/spa-templates @@ -1 +1 @@ -Subproject commit 3d16ec44cf4bc171531cefb0a81ae72b8bf0c478 +Subproject commit 08a4e743ccec97c24aec8b7a719655f08e827d8e