diff --git a/.azure/pipelines/jobs/default-build.yml b/.azure/pipelines/jobs/default-build.yml
index a136f3871f..d218c44531 100644
--- a/.azure/pipelines/jobs/default-build.yml
+++ b/.azure/pipelines/jobs/default-build.yml
@@ -55,6 +55,7 @@ parameters:
artifacts: []
buildDirectory: ''
buildScript: ''
+ installTar: true
installNodeJs: true
installJdk: true
timeoutInMinutes: 180
@@ -151,6 +152,9 @@ jobs:
Write-Host "##vso[task.setvariable variable=SeleniumProcessTrackingFolder]$(BuildDirectory)\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
- ${{ parameters.beforeBuild }}
diff --git a/build.ps1 b/build.ps1
index 17020044ed..c515a84af5 100644
--- a/build.ps1
+++ b/build.ps1
@@ -307,6 +307,8 @@ if (-not $foundJdk -and $RunBuild -and ($All -or $BuildJava) -and -not $NoBuildJ
# Initialize global variables need to be set before the import of Arcade is imported
$restore = $RunRestore
+# Though VS Code may indicate $nodeReuse, $warnAsError and $msbuildEngine are unused, tools.ps1 uses them.
+
# Disable node reuse - Workaround perpetual issues in node reuse and custom task assemblies
$nodeReuse = $false
$env:MSBUILDDISABLENODEREUSE=1
@@ -328,10 +330,10 @@ if ($CI) {
}
# tools.ps1 corrupts global state, so reset these values in case they carried over from a previous build
-rm variable:global:_BuildTool -ea Ignore
-rm variable:global:_DotNetInstallDir -ea Ignore
-rm variable:global:_ToolsetBuildProj -ea Ignore
-rm variable:global:_MSBuildExe -ea Ignore
+Remove-Item variable:global:_BuildTool -ea Ignore
+Remove-Item variable:global:_DotNetInstallDir -ea Ignore
+Remove-Item variable:global:_ToolsetBuildProj -ea Ignore
+Remove-Item variable:global:_MSBuildExe -ea Ignore
# Import Arcade
. "$PSScriptRoot/eng/common/tools.ps1"
@@ -391,10 +393,10 @@ finally {
}
# tools.ps1 corrupts global state, so reset these values so they don't carry between invocations of build.ps1
- rm variable:global:_BuildTool -ea Ignore
- rm variable:global:_DotNetInstallDir -ea Ignore
- rm variable:global:_ToolsetBuildProj -ea Ignore
- rm variable:global:_MSBuildExe -ea Ignore
+ Remove-Item variable:global:_BuildTool -ea Ignore
+ Remove-Item variable:global:_DotNetInstallDir -ea Ignore
+ Remove-Item variable:global:_ToolsetBuildProj -ea Ignore
+ Remove-Item variable:global:_MSBuildExe -ea Ignore
if ($DumpProcesses -or $ci) {
Stop-Job -Name DumpProcesses
diff --git a/docs/BuildFromSource.md b/docs/BuildFromSource.md
index 7740ec6140..5cc74a5f9d 100644
--- a/docs/BuildFromSource.md
+++ b/docs/BuildFromSource.md
@@ -27,6 +27,7 @@ Building ASP.NET Core on Windows requires:
```ps1
PS> ./eng/scripts/InstallJdk.ps1
```
+* Chrome - Selenium-based tests require a version of Chrome to be installed. Download and install it from [https://www.google.com/chrome]
### macOS/Linux
diff --git a/eng/Baseline.Designer.props b/eng/Baseline.Designer.props
index f35501e1de..0b3cfa0c07 100644
--- a/eng/Baseline.Designer.props
+++ b/eng/Baseline.Designer.props
@@ -2,7 +2,7 @@
$(MSBuildAllProjects);$(MSBuildThisFileFullPath)
- 2.2.6
+ 2.2.7
@@ -77,12 +77,12 @@
- 2.2.6
+ 2.2.7
- 2.2.6
+ 2.2.7
@@ -303,10 +303,11 @@
- 2.2.0
+ 2.2.7
+
@@ -434,21 +435,21 @@
- 2.2.0
+ 2.2.7
-
-
+
+
-
+
@@ -1197,12 +1198,12 @@
- 2.2.0
+ 2.2.7
-
+
diff --git a/eng/Baseline.xml b/eng/Baseline.xml
index 0946044016..b541b834a2 100644
--- a/eng/Baseline.xml
+++ b/eng/Baseline.xml
@@ -4,7 +4,7 @@ This file contains a list of all the packages and their versions which were rele
build of ASP.NET Core 2.2.x. Update this list when preparing for a new patch.
-->
-
+
@@ -12,8 +12,8 @@ build of ASP.NET Core 2.2.x. Update this list when preparing for a new patch.
-
-
+
+
@@ -39,7 +39,7 @@ build of ASP.NET Core 2.2.x. Update this list when preparing for a new patch.
-
+
@@ -53,7 +53,7 @@ build of ASP.NET Core 2.2.x. Update this list when preparing for a new patch.
-
+
@@ -122,7 +122,7 @@ build of ASP.NET Core 2.2.x. Update this list when preparing for a new patch.
-
+
diff --git a/eng/PatchConfig.props b/eng/PatchConfig.props
index 6cfba9b0b6..2c26521f8c 100644
--- a/eng/PatchConfig.props
+++ b/eng/PatchConfig.props
@@ -29,10 +29,6 @@ Later on, this will be checked using this condition:
java:signalr;
-
-
-
-
@aspnet/signalr;
@@ -70,4 +66,8 @@ Later on, this will be checked using this condition:
Microsoft.AspNetCore.SpaServices;
+
+
+
+
diff --git a/eng/Versions.props b/eng/Versions.props
index 4ffd700107..fff5d0df67 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -9,7 +9,7 @@
5
0
0
- 1
+ 2
diff --git a/eng/scripts/InstallTar.ps1 b/eng/scripts/InstallTar.ps1
new file mode 100644
index 0000000000..12159a8d0b
--- /dev/null
+++ b/eng/scripts/InstallTar.ps1
@@ -0,0 +1,74 @@
+<#
+.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/eng/targets/ReferenceAssembly.targets b/eng/targets/ReferenceAssembly.targets
index f516f1b07a..765cc16932 100644
--- a/eng/targets/ReferenceAssembly.targets
+++ b/eng/targets/ReferenceAssembly.targets
@@ -61,15 +61,21 @@
+ <_GenApiFile>$([MSBuild]::NormalizePath('$(ArtifactsDir)', 'log', 'GenAPI.rsp'))
<_GenAPICommand Condition="'$(MSBuildRuntimeType)' == 'core'">"$(DotNetTool)" --roll-forward-on-no-candidate-fx 2 "$(_GenAPIPath)"
- <_GenAPICmd>$(_GenAPICommand)
- <_GenAPICmd>$(_GenAPICmd) "$(TargetPath)"
- <_GenAPICmd>$(_GenAPICmd) --lib-path "@(_ReferencePathDirectories)"
- <_GenAPICmd>$(_GenAPICmd) --out "$(_RefSourceFileOutputPath)"
- <_GenAPICmd>$(_GenAPICmd) --header-file "$(RepoRoot)/eng/LicenseHeader.txt"
- <_GenAPICmd>$(_GenAPICmd) --exclude-api-list "$(RepoRoot)/eng/GenAPI.exclusions.txt"
+ <_GenAPICmd>$(_GenAPICommand) @"$(_GenApiFile)"
+ <_GenApiArguments>
+
+
+
@@ -96,4 +102,4 @@
-
\ No newline at end of file
+
diff --git a/global.json b/global.json
index 1ff988fe32..b0204615a6 100644
--- a/global.json
+++ b/global.json
@@ -12,6 +12,7 @@
"$(MicrosoftNETCoreAppRuntimeVersion)"
]
},
+ "Git": "2.22.0",
"jdk": "11.0.3",
"vs": {
"version": "16.0",
diff --git a/src/Components/Ignitor/src/BlazorClient.cs b/src/Components/Ignitor/src/BlazorClient.cs
index 861e2ad0f2..df93faa3b3 100644
--- a/src/Components/Ignitor/src/BlazorClient.cs
+++ b/src/Components/Ignitor/src/BlazorClient.cs
@@ -88,7 +88,7 @@ namespace Ignitor
public Task PrepareForNextBatch(TimeSpan? timeout)
{
- if (NextBatchReceived?.Completion != null)
+ if (NextBatchReceived != null && !NextBatchReceived.Disposed)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
@@ -100,7 +100,7 @@ namespace Ignitor
public Task PrepareForNextJSInterop(TimeSpan? timeout)
{
- if (NextJSInteropReceived?.Completion != null)
+ if (NextJSInteropReceived != null && !NextJSInteropReceived.Disposed)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
@@ -112,7 +112,7 @@ namespace Ignitor
public Task PrepareForNextDotNetInterop(TimeSpan? timeout)
{
- if (NextDotNetInteropCompletionReceived?.Completion != null)
+ if (NextDotNetInteropCompletionReceived != null && !NextDotNetInteropCompletionReceived.Disposed)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
@@ -124,7 +124,7 @@ namespace Ignitor
public Task PrepareForNextCircuitError(TimeSpan? timeout)
{
- if (NextErrorReceived?.Completion != null)
+ if (NextErrorReceived != null && !NextErrorReceived.Disposed)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
@@ -136,7 +136,7 @@ namespace Ignitor
public Task PrepareForNextDisconnect(TimeSpan? timeout)
{
- if (NextDisconnect?.Completion != null)
+ if (NextDisconnect != null && !NextDisconnect.Disposed)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
@@ -499,56 +499,6 @@ namespace Ignitor
return element;
}
- private class CancellableOperation
- {
- public CancellableOperation(TimeSpan? timeout)
- {
- Timeout = timeout;
- Initialize();
- }
-
- public TimeSpan? Timeout { get; }
-
- public TaskCompletionSource Completion { get; set; }
-
- public CancellationTokenSource Cancellation { get; set; }
-
- public CancellationTokenRegistration CancellationRegistration { get; set; }
-
- private void Initialize()
- {
- Completion = new TaskCompletionSource(TaskContinuationOptions.RunContinuationsAsynchronously);
- Completion.Task.ContinueWith(
- (task, state) =>
- {
- var operation = (CancellableOperation)state;
- operation.Dispose();
- },
- this,
- TaskContinuationOptions.ExecuteSynchronously); // We need to execute synchronously to clean-up before anything else continues
- if (Timeout != null && Timeout != System.Threading.Timeout.InfiniteTimeSpan && Timeout != TimeSpan.MaxValue)
- {
- Cancellation = new CancellationTokenSource(Timeout.Value);
- CancellationRegistration = Cancellation.Token.Register(
- (self) =>
- {
- var operation = (CancellableOperation)self;
- operation.Completion.TrySetCanceled(operation.Cancellation.Token);
- operation.Cancellation.Dispose();
- operation.CancellationRegistration.Dispose();
- },
- this);
- }
- }
-
- private void Dispose()
- {
- Completion = null;
- Cancellation.Dispose();
- CancellationRegistration.Dispose();
- }
- }
-
private string[] ReadMarkers(string content)
{
content = content.Replace("\r\n", "").Replace("\n", "");
diff --git a/src/Components/Ignitor/src/CancellableOperation.cs b/src/Components/Ignitor/src/CancellableOperation.cs
new file mode 100644
index 0000000000..dad8cb5587
--- /dev/null
+++ b/src/Components/Ignitor/src/CancellableOperation.cs
@@ -0,0 +1,64 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ignitor
+{
+ internal class CancellableOperation
+ {
+ public CancellableOperation(TimeSpan? timeout)
+ {
+ Timeout = timeout;
+
+ Completion = new TaskCompletionSource(TaskContinuationOptions.RunContinuationsAsynchronously);
+ Completion.Task.ContinueWith(
+ (task, state) =>
+ {
+ var operation = (CancellableOperation)state;
+ operation.Dispose();
+ },
+ this,
+ TaskContinuationOptions.ExecuteSynchronously); // We need to execute synchronously to clean-up before anything else continues
+
+ if (Timeout != null && Timeout != System.Threading.Timeout.InfiniteTimeSpan && Timeout != TimeSpan.MaxValue)
+ {
+ Cancellation = new CancellationTokenSource(Timeout.Value);
+ CancellationRegistration = Cancellation.Token.Register(
+ (self) =>
+ {
+ var operation = (CancellableOperation)self;
+ operation.Completion.TrySetCanceled(operation.Cancellation.Token);
+ operation.Cancellation.Dispose();
+ operation.CancellationRegistration.Dispose();
+ },
+ this);
+ }
+ }
+
+ public TimeSpan? Timeout { get; }
+
+ public TaskCompletionSource Completion { get; }
+
+ public CancellationTokenSource Cancellation { get; }
+
+ public CancellationTokenRegistration CancellationRegistration { get; }
+
+ public bool Disposed { get; private set; }
+
+ private void Dispose()
+ {
+ if (Disposed)
+ {
+ return;
+ }
+
+ Disposed = true;
+ Completion.TrySetCanceled(Cancellation.Token);
+ Cancellation.Dispose();
+ CancellationRegistration.Dispose();
+ }
+ }
+}
diff --git a/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj b/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj
index 00c9026c5a..a60b73a3cd 100644
--- a/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj
+++ b/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj
@@ -171,14 +171,26 @@ This package is an internal implementation of the .NET Core SDK and is not meant
Inputs="@(RefPackContent)"
Outputs="$(ZipArchiveOutputPath);$(TarArchiveOutputPath)"
Condition="'$(IsPackable)' == 'true'">
+
+ <_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(':',''))
+
+
+
-
+
+
+
diff --git a/src/Shared/E2ETesting/BrowserFixture.cs b/src/Shared/E2ETesting/BrowserFixture.cs
index a453a2d7be..8b3239c5df 100644
--- a/src/Shared/E2ETesting/BrowserFixture.cs
+++ b/src/Shared/E2ETesting/BrowserFixture.cs
@@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.E2ETesting
var instance = await SeleniumStandaloneServer.GetInstanceAsync(output);
var attempt = 0;
- var maxAttempts = 3;
+ const int maxAttempts = 3;
do
{
try
@@ -132,18 +132,16 @@ namespace Microsoft.AspNetCore.E2ETesting
return (driver, logs);
}
- catch
+ catch (Exception ex)
{
- if (attempt >= maxAttempts)
- {
- throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is irresponsive");
- }
+ output.WriteLine($"Error initializing RemoteWebDriver: {ex.Message}");
}
+
attempt++;
+
} while (attempt < maxAttempts);
- // We will never get here. Keeping the compiler happy.
- throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is unresponsive");
+ throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is irresponsive");
}
}
}
diff --git a/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs b/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs
index 1b6abc9f27..9e076cefc3 100644
--- a/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs
+++ b/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs
@@ -13,7 +13,7 @@ namespace Microsoft.Extensions.ApiDescription.Client
public void Execute_ReturnsExpectedItem()
{
// Arrange
- string input = "Identity=../files/azureMonitor.json|ClassName=azureMonitorClient|" +
+ var input = "Identity=../files/azureMonitor.json|ClassName=azureMonitorClient|" +
"CodeGenerator=NSwagCSharp|Namespace=ConsoleClient|Options=|OutputPath=" +
"C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs|" +
"OriginalItemSpec=../files/azureMonitor.json|FirstForGenerator=true";
@@ -22,8 +22,8 @@ namespace Microsoft.Extensions.ApiDescription.Client
Input = input,
};
- string expectedIdentity = "../files/azureMonitor.json";
- IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
+ var expectedIdentity = "../files/azureMonitor.json";
+ var expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
{
{ "ClassName", "azureMonitorClient" },
{ "CodeGenerator", "NSwagCSharp" },
diff --git a/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs b/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs
index 7bc5cc7fb9..3602ea1c56 100644
--- a/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs
+++ b/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs
@@ -27,7 +27,7 @@ namespace Microsoft.Extensions.ApiDescription.Client
OutputDirectory = "obj",
};
- IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
+ var expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
{
{ "ClassName", "NSwagClient" },
{ "CodeGenerator", "NSwagCSharp" },
@@ -85,7 +85,7 @@ namespace Microsoft.Extensions.ApiDescription.Client
OutputDirectory = "obj",
};
- IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
+ var expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
{
{ "ClassName", className },
{ "CodeGenerator", "NSwagCSharp" },
@@ -143,7 +143,7 @@ namespace Microsoft.Extensions.ApiDescription.Client
OutputDirectory = "obj",
};
- IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
+ var expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
{
{ "ClassName", "NSwagClient" },
{ "CodeGenerator", "NSwagCSharp" },
@@ -201,7 +201,7 @@ namespace Microsoft.Extensions.ApiDescription.Client
OutputDirectory = "bin",
};
- IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
+ var expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
{
{ "ClassName", className },
{ "CodeGenerator", "NSwagCSharp" },
@@ -351,7 +351,7 @@ namespace Microsoft.Extensions.ApiDescription.Client
OutputDirectory = "bin",
};
- IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
+ var expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
{
{ "ClassName", className },
{ "CodeGenerator", "NSwagCSharp" },
@@ -414,7 +414,7 @@ namespace Microsoft.Extensions.ApiDescription.Client
OutputDirectory = "bin",
};
- IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
+ var expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
{
{ "ClassName", className },
{ "CodeGenerator", "NSwagCSharp" },
@@ -481,7 +481,7 @@ namespace Microsoft.Extensions.ApiDescription.Client
OutputDirectory = "obj",
};
- IDictionary expectedMetadata1 = new SortedDictionary(StringComparer.Ordinal)
+ var expectedMetadata1 = new SortedDictionary(StringComparer.Ordinal)
{
{ "ClassName", className12 },
{ "CodeGenerator", codeGenerator13 },
@@ -496,7 +496,7 @@ namespace Microsoft.Extensions.ApiDescription.Client
$"OutputPath={outputPath1}|ClassName={className12}|Namespace={@namespace}"
},
};
- IDictionary expectedMetadata2 = new SortedDictionary(StringComparer.Ordinal)
+ var expectedMetadata2 = new SortedDictionary(StringComparer.Ordinal)
{
{ "ClassName", className12 },
{ "CodeGenerator", codeGenerator2 },
@@ -511,7 +511,7 @@ namespace Microsoft.Extensions.ApiDescription.Client
$"OutputPath={outputPath2}|ClassName={className12}|Namespace={@namespace}"
},
};
- IDictionary expectedMetadata3 = new SortedDictionary(StringComparer.Ordinal)
+ var expectedMetadata3 = new SortedDictionary(StringComparer.Ordinal)
{
{ "ClassName", className3 },
{ "CodeGenerator", codeGenerator13 },
diff --git a/src/Tools/Extensions.ApiDescription.Client/test/MetadataSerializerTest.cs b/src/Tools/Extensions.ApiDescription.Client/test/MetadataSerializerTest.cs
new file mode 100644
index 0000000000..adc889ccac
--- /dev/null
+++ b/src/Tools/Extensions.ApiDescription.Client/test/MetadataSerializerTest.cs
@@ -0,0 +1,314 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Extensions.ApiDescription.Client
+{
+ // ItemSpec values always have '\\' converted to '/' on input when running on non-Windows. It is not possible to
+ // retrieve the original (unconverted) item spec value. In other respects, item spec values are treated identically
+ // to custom metadata values.
+ //
+ // ITaskItem members aka the implicitly-implemented methods and properties in TaskItem expect _escaped_ values on
+ // input and return _literal_ values. This includes TaskItem constructors and CloneCustomMetadata() (which returns
+ // a new dictionary containing literal values). TaskItem stores all values in their escaped form.
+ //
+ // Added ITaskItem2 members e.g. CloneCustomMetadataEscaped(), GetMetadataValueEscaped(...) and
+ // EvaluatedIncludeEscaped return escaped values. Of all TaskItem methods, only SetMetadataValueLiteral(...)
+ // accepts a literal input value.
+ //
+ // Metadata names are never escaped.
+ //
+ // MetadataSerializer expects literal values on input.
+ public class MetadataSerializerTest
+ {
+ // Maps literal to escaped values.
+ public static TheoryData EscapedValuesMapping { get; } = new TheoryData
+ {
+ { "No escaping necessary for =.", "No escaping necessary for =." },
+ { "Value needs escaping? (yes)", "Value needs escaping%3f %28yes%29" },
+ { "$ comes earlier; @ comes later.", "%24 comes earlier%3b %40 comes later." },
+ {
+ "A '%' *character* needs escaping %-escaping.",
+ "A %27%25%27 %2acharacter%2a needs escaping %25-escaping."
+ },
+ };
+
+ public static TheoryData EscapedValues
+ {
+ get
+ {
+ var result = new TheoryData();
+ foreach (var entry in EscapedValuesMapping)
+ {
+ result.Add((string)entry[1]);
+ }
+
+ return result;
+ }
+ }
+
+ public static TheoryData LiteralValues
+ {
+ get
+ {
+ var result = new TheoryData();
+ foreach (var entry in EscapedValuesMapping)
+ {
+ result.Add((string)entry[0]);
+ }
+
+ return result;
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(LiteralValues))]
+ public void SetMetadata_UpdatesTaskAsExpected(string value)
+ {
+ // Arrange
+ var item = new TaskItem("My Identity");
+ var key = "My key";
+
+ // Act
+ MetadataSerializer.SetMetadata(item, key, value);
+
+ // Assert
+ Assert.Equal(value, item.GetMetadata(key));
+ }
+
+ [Theory]
+ [MemberData(nameof(EscapedValuesMapping))]
+ public void SetMetadata_UpdatesTaskAsExpected_WithLegacyItem(string value, string escapedValue)
+ {
+ // Arrange
+ var item = new Mock(MockBehavior.Strict);
+ var key = "My key";
+ item.Setup(i => i.SetMetadata(key, escapedValue)).Verifiable();
+
+ // Act
+ MetadataSerializer.SetMetadata(item.Object, key, value);
+
+ // Assert
+ item.Verify(i => i.SetMetadata(key, escapedValue), Times.Once);
+ }
+
+ [Fact]
+ public void DeserializeMetadata_ReturnsExpectedTask()
+ {
+ // Arrange
+ var identity = "../files/azureMonitor.json";
+ var input = $"Identity={identity}|ClassName=azureMonitorClient|" +
+ "CodeGenerator=NSwagCSharp|FirstForGenerator=true|Namespace=ConsoleClient|" +
+ "Options=|OriginalItemSpec=../files/azureMonitor.json|" +
+ "OutputPath=C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs";
+
+ var expectedMetadata = new SortedDictionary(StringComparer.Ordinal)
+ {
+ { "ClassName", "azureMonitorClient" },
+ { "CodeGenerator", "NSwagCSharp" },
+ { "FirstForGenerator", "true" },
+ { "Namespace", "ConsoleClient" },
+ { "Options", "" },
+ { "OriginalItemSpec", identity },
+ { "OutputPath", "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs" },
+ };
+
+ // Act
+ var item = MetadataSerializer.DeserializeMetadata(input);
+
+ // Assert
+ Assert.Equal(identity, item.ItemSpec);
+ var metadata = Assert.IsAssignableFrom>(item.CloneCustomMetadata());
+
+ // The dictionary CloneCustomMetadata returns doesn't provide a useful KeyValuePair enumerator.
+ var orderedMetadata = new SortedDictionary(StringComparer.Ordinal);
+ foreach (var key in metadata.Keys)
+ {
+ orderedMetadata.Add(key, metadata[key]);
+ }
+
+ Assert.Equal(expectedMetadata, orderedMetadata);
+
+ }
+
+ [Theory]
+ [MemberData(nameof(EscapedValuesMapping))]
+ public void DeserializeMetadata_ReturnsExpectedTask_WhenEscaping(string value, string escapedValue)
+ {
+ // Arrange
+ var identity = "../files/azureMonitor.json";
+ var input = $"Identity={identity}|Value={escapedValue}";
+
+ // Act
+ var item = MetadataSerializer.DeserializeMetadata(input);
+
+ // Assert
+ Assert.Equal(identity, item.ItemSpec);
+ Assert.Equal(value, item.GetMetadata("Value"));
+ }
+
+ [Theory]
+ [MemberData(nameof(EscapedValuesMapping))]
+ public void DeserializeMetadata_ReturnsExpectedTask_WhenEscapingIdentity(string value, string escapedValue)
+ {
+ // Arrange
+ var input = $"Identity={escapedValue}|Value=a value";
+
+ // Act
+ var item = MetadataSerializer.DeserializeMetadata(input);
+
+ // Assert
+ Assert.Equal(value, item.ItemSpec);
+ Assert.Equal("a value", item.GetMetadata("Value"));
+ }
+
+ [Fact]
+ public void SerializeMetadata_ReturnsExpectedString()
+ {
+ // Arrange
+ var identity = "../files/azureMonitor.json";
+ var metadata = new SortedDictionary(StringComparer.Ordinal)
+ {
+ { "ClassName", "azureMonitorClient" },
+ { "CodeGenerator", "NSwagCSharp" },
+ { "FirstForGenerator", "true" },
+ { "Namespace", "ConsoleClient" },
+ { "Options", "" },
+ { "OriginalItemSpec", identity },
+ { "OutputPath", "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs" },
+ };
+
+ var input = new TaskItem(identity, metadata);
+ var expectedResult = $"Identity={identity}|ClassName=azureMonitorClient|" +
+ "CodeGenerator=NSwagCSharp|FirstForGenerator=true|Namespace=ConsoleClient|" +
+ "Options=|OriginalItemSpec=../files/azureMonitor.json|" +
+ "OutputPath=C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs";
+
+ // Act
+ var result = MetadataSerializer.SerializeMetadata(input);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Theory]
+ [MemberData(nameof(EscapedValues))]
+ public void SerializeMetadata_ReturnsExpectedString_WhenEscaping(string escapedValue)
+ {
+ // Arrange
+ var identity = "../files/azureMonitor.json";
+ var expectedResult = $"Identity={identity}|Value={escapedValue}";
+ var metadata = new SortedDictionary(StringComparer.Ordinal) { { "Value", escapedValue } };
+ var input = new TaskItem(identity, metadata);
+
+ // Act
+ var result = MetadataSerializer.SerializeMetadata(input);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Theory]
+ [MemberData(nameof(EscapedValues))]
+ public void SerializeMetadata_ReturnsExpectedString_WhenEscapingIdentity(string escapedValue)
+ {
+ // Arrange
+ var metadata = new SortedDictionary(StringComparer.Ordinal) { { "Value", "a value" } };
+ var expectedResult = $"Identity={escapedValue}|Value=a value";
+ var input = new TaskItem(escapedValue, metadata);
+
+ // Act
+ var result = MetadataSerializer.SerializeMetadata(input);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void SerializeMetadata_ReturnsExpectedString_WithLegacyItem()
+ {
+ // Arrange
+ var identity = "../files/azureMonitor.json";
+ var metadata = new SortedDictionary(StringComparer.Ordinal)
+ {
+ { "ClassName", "azureMonitorClient" },
+ { "CodeGenerator", "NSwagCSharp" },
+ { "FirstForGenerator", "true" },
+ { "Namespace", "ConsoleClient" },
+ { "Options", "" },
+ { "OriginalItemSpec", identity },
+ { "OutputPath", "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs" },
+ };
+
+ var input = new Mock(MockBehavior.Strict);
+ input.SetupGet(i => i.ItemSpec).Returns(identity).Verifiable();
+ input.Setup(i => i.CloneCustomMetadata()).Returns(metadata).Verifiable();
+
+ var expectedResult = $"Identity={identity}|ClassName=azureMonitorClient|" +
+ "CodeGenerator=NSwagCSharp|FirstForGenerator=true|Namespace=ConsoleClient|" +
+ "Options=|OriginalItemSpec=../files/azureMonitor.json|" +
+ "OutputPath=C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs";
+
+ // Act
+ var result = MetadataSerializer.SerializeMetadata(input.Object);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ input.VerifyGet(i => i.ItemSpec, Times.Once);
+ input.Verify(i => i.CloneCustomMetadata(), Times.Once);
+ }
+
+ [Theory]
+ [MemberData(nameof(EscapedValuesMapping))]
+ public void SerializeMetadata_ReturnsExpectedString_WithLegacyItem_WhenEscaping(
+ string value,
+ string escapedValue)
+ {
+ // Arrange
+ var identity = "../files/azureMonitor.json";
+ var metadata = new SortedDictionary(StringComparer.Ordinal) { { "Value", value } };
+ var input = new Mock(MockBehavior.Strict);
+ input.SetupGet(i => i.ItemSpec).Returns(identity).Verifiable();
+ input.Setup(i => i.CloneCustomMetadata()).Returns(metadata).Verifiable();
+
+ var expectedResult = $"Identity={identity}|Value={escapedValue}";
+
+ // Act
+ var result = MetadataSerializer.SerializeMetadata(input.Object);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ input.VerifyGet(i => i.ItemSpec, Times.Once);
+ input.Verify(i => i.CloneCustomMetadata(), Times.Once);
+ }
+
+ [Theory]
+ [MemberData(nameof(EscapedValuesMapping))]
+ public void SerializeMetadata_ReturnsExpectedString_WithLegacyItem_WhenEscapingIdentity(
+ string value,
+ string escapedValue)
+ {
+ // Arrange
+ var metadata = new SortedDictionary(StringComparer.Ordinal) { { "Value", "a value" } };
+ var input = new Mock(MockBehavior.Strict);
+ input.SetupGet(i => i.ItemSpec).Returns(value).Verifiable();
+ input.Setup(i => i.CloneCustomMetadata()).Returns(metadata).Verifiable();
+
+ var expectedResult = $"Identity={escapedValue}|Value=a value";
+
+ // Act
+ var result = MetadataSerializer.SerializeMetadata(input.Object);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ input.VerifyGet(i => i.ItemSpec, Times.Once);
+ input.Verify(i => i.CloneCustomMetadata(), Times.Once);
+ }
+ }
+}
diff --git a/src/Tools/Microsoft.dotnet-openapi/src/Commands/BaseCommand.cs b/src/Tools/Microsoft.dotnet-openapi/src/Commands/BaseCommand.cs
index 7123f0d04e..92fe39df4f 100644
--- a/src/Tools/Microsoft.dotnet-openapi/src/Commands/BaseCommand.cs
+++ b/src/Tools/Microsoft.dotnet-openapi/src/Commands/BaseCommand.cs
@@ -6,9 +6,12 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
+using System.Net;
+using System.Net.Http;
using System.Reflection;
using System.Security.Cryptography;
using System.Text.Json;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Evaluation;
using Microsoft.DotNet.Openapi.Tools;
@@ -240,13 +243,13 @@ namespace Microsoft.DotNet.OpenApi.Commands
internal async Task DownloadToFileAsync(string url, string destinationPath, bool overwrite)
{
- using var response = await _httpClient.GetResponseAsync(url);
+ using var response = await RetryRequest(() => _httpClient.GetResponseAsync(url));
await WriteToFileAsync(await response.Stream, destinationPath, overwrite);
}
internal async Task DownloadGivenOption(string url, CommandOption fileOption)
{
- using var response = await _httpClient.GetResponseAsync(url);
+ using var response = await RetryRequest(() => _httpClient.GetResponseAsync(url));
if (response.IsSuccessCode())
{
@@ -272,6 +275,56 @@ namespace Microsoft.DotNet.OpenApi.Commands
}
}
+ ///
+ /// Retries every 1 sec for 60 times by default.
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static async Task RetryRequest(
+ Func> retryBlock,
+ CancellationToken cancellationToken = default,
+ int retryCount = 60)
+ {
+ for (var retry = 0; retry < retryCount; retry++)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ throw new OperationCanceledException("Failed to connect, retry canceled.", cancellationToken);
+ }
+
+ try
+ {
+ var response = await retryBlock().ConfigureAwait(false);
+
+ if (response.StatusCode == HttpStatusCode.ServiceUnavailable)
+ {
+ // Automatically retry on 503. May be application is still booting.
+ continue;
+ }
+
+ return response; // Went through successfully
+ }
+ catch (Exception exception)
+ {
+ if (retry == retryCount - 1)
+ {
+ throw;
+ }
+ else
+ {
+ if (exception is HttpRequestException || exception is WebException)
+ {
+ await Task.Delay(1 * 1000); //Wait for a while before retry.
+ }
+ }
+ }
+ }
+
+ throw new OperationCanceledException("Failed to connect, retry limit exceeded.");
+ }
+
private string GetUniqueFileName(string directory, string fileName, string extension)
{
var uniqueName = fileName;
diff --git a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs
index 95369d7683..ff8000263d 100644
--- a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs
+++ b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs
@@ -440,8 +440,8 @@ namespace Microsoft.DotNet.OpenApi.Add.Tests
var url = BrokenUrl;
var run = app.Execute(new[] { "add", "url", url });
- Assert.Equal(_error.ToString(), $"The given url returned 'NotFound', " +
- "indicating failure. The url might be wrong, or there might be a networking issue."+Environment.NewLine);
+ Assert.Equal($"The given url returned 'NotFound', " +
+ "indicating failure. The url might be wrong, or there might be a networking issue."+Environment.NewLine, _error.ToString());
Assert.Equal(1, run);
var expectedJsonName = "dingos.json";