OpenAPI ServiceReference Tool (#12810)

Add ServiceReference tool
This commit is contained in:
Ryan Brandenburg 2019-08-22 11:16:16 -07:00 committed by Kevin Pilch
parent 1b2010768e
commit cfcffd8251
53 changed files with 3659 additions and 96 deletions

14
.vscode/settings.json vendored
View File

@ -1,9 +1,9 @@
{
"files.trimTrailingWhitespace": true,
"files.associations": {
"*.*proj": "xml",
"*.props": "xml",
"*.targets": "xml",
"*.tasks": "xml"
}
"files.trimTrailingWhitespace": true,
"files.associations": {
"*.*proj": "xml",
"*.props": "xml",
"*.targets": "xml",
"*.tasks": "xml"
}
}

View File

@ -28,6 +28,7 @@
<!-- These projects are meant to be executed by tests. -->
<ProjectToExclude Include="
$(RepoRoot)src\Tools\dotnet-watch\test\TestProjects\**\*.csproj;
$(RepoRoot)src\Tools\Tests.Common\TestProjects\**\*.csproj;
$(RepoRoot)src\Razor\Razor.Design\test\testassets\**\*.*proj;
$(RepoRoot)src\submodules\**\*.*proj;
$(RepoRoot)src\Installers\**\*.*proj;

View File

@ -149,7 +149,9 @@ and are generated based on the last package release.
</ItemGroup>
<ItemGroup Label="MSBuild">
<LatestPackageReference Include="Microsoft.Build" Version="$(MicrosoftBuildPackageVersion)" />
<LatestPackageReference Include="Microsoft.Build.Framework" Version="$(MicrosoftBuildFrameworkPackageVersion)" />
<LatestPackageReference Include="Microsoft.Build.Locator" Version="$(MicrosoftBuildLocatorPackageVersion)" />
<LatestPackageReference Include="Microsoft.Build.Utilities.Core" Version="$(MicrosoftBuildUtilitiesCorePackageVersion)" />
</ItemGroup>
@ -172,6 +174,7 @@ and are generated based on the last package release.
<LatestPackageReference Include="Moq" Version="$(MoqPackageVersion)" />
<LatestPackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
<LatestPackageReference Include="Newtonsoft.Json.Bson" Version="$(NewtonsoftJsonBsonPackageVersion)" />
<LatestPackageReference Include="NSwag.ApiDescription.Client" Version="$(NSwagApiDescriptionClientPackageVersion)" />
<LatestPackageReference Include="Selenium.Support" Version="$(SeleniumSupportPackageVersion)" />
<LatestPackageReference Include="Selenium.WebDriver" Version="$(SeleniumWebDriverPackageVersion)" />
<LatestPackageReference Include="Selenium.WebDriver.ChromeDriver" Version="$(SeleniumWebDriverChromeDriverPackageVersion)" />

View File

@ -97,6 +97,12 @@
<_DotNetFilesToExclude Include="$(RedistNetCorePath)dotnet.exe" CertificateName="None" />
<FileSignInfo Include="@(_DotNetFilesToExclude->'%(FileName)%(Extension)'->Distinct())" CertificateName="None" />
<!--
We include the Microsoft.Build.Locator.dll assembly in our global tool 'Microsoft.dotnet-openapi'.
It is already signed by that team, so we don't need to sign it.
-->
<FileSignInfo Include="Microsoft.Build.Locator.dll" CertificateName="None" />
<!--
We include the Microsoft.Data.SqlClient.dll assembly in our global tool 'dotnet-sql-cache'.
It is already signed by that team, so we don't need to sign it.

View File

@ -196,7 +196,9 @@
<!-- Partner teams -->
<MicrosoftAzureKeyVaultPackageVersion>2.3.2</MicrosoftAzureKeyVaultPackageVersion>
<MicrosoftAzureStorageBlobPackageVersion>10.0.1</MicrosoftAzureStorageBlobPackageVersion>
<MicrosoftBuildPackageVersion>15.8.166</MicrosoftBuildPackageVersion>
<MicrosoftBuildFrameworkPackageVersion>15.8.166</MicrosoftBuildFrameworkPackageVersion>
<MicrosoftBuildLocatorPackageVersion>1.2.6</MicrosoftBuildLocatorPackageVersion>
<MicrosoftBuildUtilitiesCorePackageVersion>15.8.166</MicrosoftBuildUtilitiesCorePackageVersion>
<MicrosoftCodeAnalysisCommonPackageVersion>3.0.0</MicrosoftCodeAnalysisCommonPackageVersion>
<MicrosoftCodeAnalysisCSharpPackageVersion>3.0.0</MicrosoftCodeAnalysisCSharpPackageVersion>
@ -232,6 +234,7 @@
<MonoCecilPackageVersion>0.10.1</MonoCecilPackageVersion>
<NewtonsoftJsonBsonPackageVersion>1.0.2</NewtonsoftJsonBsonPackageVersion>
<NewtonsoftJsonPackageVersion>12.0.1</NewtonsoftJsonPackageVersion>
<NSwagApiDescriptionClientPackageVersion>13.0.4</NSwagApiDescriptionClientPackageVersion>
<SeleniumSupportPackageVersion>3.12.1</SeleniumSupportPackageVersion>
<SeleniumWebDriverMicrosoftDriverPackageVersion>17.17134.0</SeleniumWebDriverMicrosoftDriverPackageVersion>
<SeleniumWebDriverChromeDriverPackageVersion>2.43.0</SeleniumWebDriverChromeDriverPackageVersion>

View File

@ -1,8 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
@ -41,42 +42,56 @@ namespace Microsoft.Extensions.Internal
private static void GetAllChildIdsUnix(int parentId, ISet<int> children, TimeSpan timeout)
{
RunProcessAndWaitForExit(
"pgrep",
$"-P {parentId}",
timeout,
out var stdout);
if (!string.IsNullOrEmpty(stdout))
try
{
using (var reader = new StringReader(stdout))
{
while (true)
{
var text = reader.ReadLine();
if (text == null)
{
return;
}
RunProcessAndWaitForExit(
"pgrep",
$"-P {parentId}",
timeout,
out var stdout);
if (int.TryParse(text, out var id))
if (!string.IsNullOrEmpty(stdout))
{
using (var reader = new StringReader(stdout))
{
while (true)
{
children.Add(id);
// Recursively get the children
GetAllChildIdsUnix(id, children, timeout);
var text = reader.ReadLine();
if (text == null)
{
return;
}
if (int.TryParse(text, out var id))
{
children.Add(id);
// Recursively get the children
GetAllChildIdsUnix(id, children, timeout);
}
}
}
}
}
catch (Win32Exception ex) when (ex.Message.Contains("No such file or directory"))
{
// This probably means that pgrep isn't installed. Nothing to be done?
}
}
private static void KillProcessUnix(int processId, TimeSpan timeout)
{
RunProcessAndWaitForExit(
"kill",
$"-TERM {processId}",
timeout,
out var stdout);
try
{
RunProcessAndWaitForExit(
"kill",
$"-TERM {processId}",
timeout,
out var stdout);
}
catch (Win32Exception ex) when (ex.Message.Contains("No such file or directory"))
{
// This probably means that the process is already dead
}
}
private static void RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout)

View File

@ -0,0 +1,84 @@
# Microsoft.dotnet-openapi
`Microsoft.dotnet-openapi` is a tool for managing OpenAPI references within your project.
## Commands
### Add Commands
<!-- TODO: Restore after https://github.com/aspnet/AspNetCore/issues/12738
#### Add Project
##### Options
| Short option | Long option | Description | Example |
|-------|------|-------|---------|
| -v|--verbose | Show verbose output. |dotnet openapi add project *-v* ../Ref/ProjRef.csproj |
| -p|--project | The project to operate on. |dotnet openapi add project *--project .\Ref.csproj* ../Ref/ProjRef.csproj |
##### Arguments
| Argument | Description | Example |
|-------------|-------------|---------|
| source-file | The source to create a reference from. Must be a project file. |dotnet openapi add project *../Ref/ProjRef.csproj* | -->
#### Add File
##### Options
| Short option| Long option| Description | Example |
|-------|------|-------|---------|
| -v|--verbose | Show verbose output. |dotnet openapi add file *-v* .\OpenAPI.json |
| -p|--updateProject | The project to operate on. |dotnet openapi add file *--updateProject .\Ref.csproj* .\OpenAPI.json |
##### Arguments
| Argument | Description | Example |
|-------------|-------------|---------|
| source-file | The source to create a reference from. Must be an OpenAPI file. |dotnet openapi add file *.\OpenAPI.json* |
#### Add URL
##### Options
| Short option| Long option| Description | Example |
|-------|------|-------------|---------|
| -v|--verbose | Show verbose output. |dotnet openapi add url *-v* <http://contoso.com/openapi.json> |
| -p|--updateProject | The project to operate on. |dotnet openapi add url *--updateProject .\Ref.csproj* <http://contoso.com/openapi.json> |
| -o|--output-file | Where to place the local copy of the OpenAPI file. |dotnet openapi add url <https://contoso.com/openapi.json> *--output-file myclient.json* |
##### Arguments
| Argument | Description | Example |
|-------------|-------------|---------|
| source-file | The source to create a reference from. Must be a URL. |dotnet openapi add url <https://contoso.com/openapi.json> |
### Remove
##### Options
| Short option| Long option| Description| Example |
|-------|------|------------|---------|
| -v|--verbose | Show verbose output. |dotnet openapi remove *-v*|
| -p|--updateProject | The project to operate on. |dotnet openapi remove *--updateProject .\Ref.csproj* .\OpenAPI.json |
#### Arguments
| Argument | Description| Example |
| ------------|------------|---------|
| source-file | The source to remove the reference to. |dotnet openapi remove *.\OpenAPI.json* |
### Refresh
#### Options
| Short option| Long option| Description | Example |
|-------|------|-------------|---------|
| -v|--verbose | Show verbose output. | dotnet openapi refresh *-v* <https://contoso.com/openapi.json> |
| -p|--updateProject | The project to operate on. | dotnet openapi refresh *--updateProject .\Ref.csproj* <https://contoso.com/openapi.json> |
#### Arguments
| Argument | Description | Example |
| ------------|-------------|---------|
| source-file | The URL to refresh the reference from. | dotnet openapi refresh *<https://contoso.com/openapi.json>* |

View File

@ -0,0 +1,104 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Reflection;
using Microsoft.Build.Locator;
using Microsoft.DotNet.Openapi.Tools;
using Microsoft.DotNet.OpenApi.Commands;
using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.DotNet.OpenApi
{
internal class Application : CommandLineApplication
{
static Application()
{
MSBuildLocator.RegisterDefaults();
}
public Application(
string workingDirectory,
IHttpClientWrapper httpClient,
TextWriter output = null,
TextWriter error = null)
{
Out = output ?? Out;
Error = error ?? Error;
WorkingDirectory = workingDirectory;
Name = "openapi";
FullName = "OpenApi reference management tool";
Description = "OpenApi reference management operations.";
ShortVersionGetter = GetInformationalVersion;
Help = HelpOption("-?|-h|--help");
Help.Inherited = true;
Invoke = () =>
{
ShowHelp();
return 0;
};
Commands.Add(new AddCommand(this, httpClient));
Commands.Add(new RemoveCommand(this, httpClient));
Commands.Add(new RefreshCommand(this, httpClient));
}
public string WorkingDirectory { get; }
public CommandOption Help { get; }
public new int Execute(params string[] args)
{
try
{
return base.Execute(args);
}
catch (AggregateException ex) when (ex.InnerException != null)
{
foreach (var innerException in ex.InnerExceptions)
{
Error.WriteLine(ex.InnerException.Message);
}
return 1;
}
catch (ArgumentException ex)
{
// Don't show a call stack when we have unneeded arguments, just print the error message.
// The code that throws this exception will print help, so no need to do it here.
Error.WriteLine(ex.Message);
return 1;
}
catch (CommandParsingException ex)
{
// Don't show a call stack when we have unneeded arguments, just print the error message.
// The code that throws this exception will print help, so no need to do it here.
Error.WriteLine(ex.Message);
return 1;
}
catch (OperationCanceledException)
{
// This is a cancellation, not a failure.
Error.WriteLine("Cancelled");
return 1;
}
catch (Exception ex)
{
Error.WriteLine(ex);
return 1;
}
}
private string GetInformationalVersion()
{
var assembly = typeof(Application).GetTypeInfo().Assembly;
var attribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
return attribute.InformationalVersion;
}
}
}

View File

@ -0,0 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.DotNet.OpenApi
{
public enum CodeGenerator
{
NSwagCSharp,
NSwagTypeScript
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.DotNet.Openapi.Tools;
namespace Microsoft.DotNet.OpenApi.Commands
{
internal class AddCommand : BaseCommand
{
private const string CommandName = "add";
public AddCommand(Application parent, IHttpClientWrapper httpClient)
: base(parent, CommandName, httpClient)
{
Commands.Add(new AddFileCommand(this, httpClient));
//TODO: Add AddprojectComand here: https://github.com/aspnet/AspNetCore/issues/12738
Commands.Add(new AddURLCommand(this, httpClient));
}
internal new Application Parent => (Application)base.Parent;
protected override Task<int> ExecuteCoreAsync()
{
ShowHelp();
return Task.FromResult(0);
}
protected override bool ValidateArguments()
{
return true;
}
}
}

View File

@ -0,0 +1,81 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.DotNet.Openapi.Tools;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.OpenApi.Commands
{
internal class AddFileCommand : BaseCommand
{
private const string CommandName = "file";
private const string SourceFileArgName = "source-file";
public AddFileCommand(AddCommand parent, IHttpClientWrapper httpClient)
: base(parent, CommandName, httpClient)
{
_codeGeneratorOption = Option("-c|--code-generator", "The code generator to use. Defaults to 'NSwagCSharp'.", CommandOptionType.SingleValue);
_sourceFileArg = Argument(SourceFileArgName, $"The OpenAPI file to add. This must be a path to local OpenAPI file(s)", multipleValues: true);
}
internal readonly CommandArgument _sourceFileArg;
internal readonly CommandOption _codeGeneratorOption;
private readonly string[] ApprovedExtensions = new[] { ".json", ".yaml", ".yml" };
protected override async Task<int> ExecuteCoreAsync()
{
var projectFilePath = ResolveProjectFile(ProjectFileOption);
Ensure.NotNullOrEmpty(_sourceFileArg.Value, SourceFileArgName);
var codeGenerator = GetCodeGenerator(_codeGeneratorOption);
foreach (var sourceFile in _sourceFileArg.Values)
{
if (!ApprovedExtensions.Any(e => sourceFile.EndsWith(e)))
{
await Warning.WriteLineAsync($"The extension for the given file '{sourceFile}' should have been one of: {string.Join(",", ApprovedExtensions)}.");
await Warning.WriteLineAsync($"The reference has been added, but may fail at build-time if the format is not correct.");
}
await AddOpenAPIReference(OpenApiReference, projectFilePath, sourceFile, codeGenerator);
}
return 0;
}
private bool IsLocalFile(string file)
{
return File.Exists(GetFullPath(file));
}
protected override bool ValidateArguments()
{
ValidateCodeGenerator(_codeGeneratorOption);
try
{
Ensure.NotNullOrEmpty(_sourceFileArg.Value, SourceFileArgName);
}
catch(ArgumentException ex)
{
Error.Write(ex.Message);
return false;
}
foreach (var sourceFile in _sourceFileArg.Values)
{
if (!IsLocalFile(sourceFile))
{
Error.Write($"{SourceFileArgName} of '{sourceFile}' could not be found.");
}
}
return true;
}
}
}

View File

@ -0,0 +1,57 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.DotNet.Openapi.Tools;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.OpenApi.Commands
{
internal class AddProjectCommand : BaseCommand
{
private const string CommandName = "project";
private const string SourceProjectArgName = "source-project";
public AddProjectCommand(BaseCommand parent, IHttpClientWrapper httpClient)
: base(parent, CommandName, httpClient)
{
_codeGeneratorOption = Option("-c|--code-generator", "The code generator to use. Defaults to 'NSwagCSharp'.", CommandOptionType.SingleValue);
_sourceProjectArg = Argument(SourceProjectArgName, $"The OpenAPI project to add. This must be the path to project file(s) containing OpenAPI endpoints", multipleValues: true);
}
internal readonly CommandArgument _sourceProjectArg;
internal readonly CommandOption _codeGeneratorOption;
protected override async Task<int> ExecuteCoreAsync()
{
var projectFilePath = ResolveProjectFile(ProjectFileOption);
var codeGenerator = GetCodeGenerator(_codeGeneratorOption);
foreach (var sourceFile in _sourceProjectArg.Values)
{
await AddOpenAPIReference(OpenApiProjectReference, projectFilePath, sourceFile, codeGenerator);
}
return 0;
}
protected override bool ValidateArguments()
{
ValidateCodeGenerator(_codeGeneratorOption);
foreach (var sourceFile in _sourceProjectArg.Values)
{
if (!IsProjectFile(sourceFile))
{
throw new ArgumentException($"{SourceProjectArgName} of '{sourceFile}' was not valid. Valid values must be project file(s)");
}
}
Ensure.NotNullOrEmpty(_sourceProjectArg.Value, SourceProjectArgName);
return true;
}
}
}

View File

@ -0,0 +1,59 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Threading.Tasks;
using Microsoft.DotNet.Openapi.Tools;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.OpenApi.Commands
{
internal class AddURLCommand : BaseCommand
{
private const string CommandName = "url";
private const string OutputFileName = "--output-file";
private const string SourceUrlArgName = "source-URL";
public AddURLCommand(AddCommand parent, IHttpClientWrapper httpClient)
: base(parent, CommandName, httpClient)
{
_codeGeneratorOption = Option("-c|--code-generator", "The code generator to use. Defaults to 'NSwagCSharp'.", CommandOptionType.SingleValue);
_outputFileOption = Option(OutputFileName, "The destination to download the remote OpenAPI file to.", CommandOptionType.SingleValue);
_sourceFileArg = Argument(SourceUrlArgName, $"The OpenAPI file to add. This must be a URL to a remote OpenAPI file.", multipleValues: true);
}
internal readonly CommandOption _outputFileOption;
internal readonly CommandArgument _sourceFileArg;
internal readonly CommandOption _codeGeneratorOption;
protected override async Task<int> ExecuteCoreAsync()
{
var projectFilePath = ResolveProjectFile(ProjectFileOption);
var sourceFile = Ensure.NotNullOrEmpty(_sourceFileArg.Value, SourceUrlArgName);
var codeGenerator = GetCodeGenerator(_codeGeneratorOption);
// We have to download the file from that URL, save it to a local file, then create a OpenApiReference
var outputFile = await DownloadGivenOption(sourceFile, _outputFileOption);
await AddOpenAPIReference(OpenApiReference, projectFilePath, outputFile, codeGenerator, sourceFile);
return 0;
}
protected override bool ValidateArguments()
{
ValidateCodeGenerator(_codeGeneratorOption);
var sourceFile = Ensure.NotNullOrEmpty(_sourceFileArg.Value, SourceUrlArgName);
if (!IsUrl(sourceFile))
{
Error.Write($"{SourceUrlArgName} was not valid. Valid values are URLs");
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,538 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Build.Evaluation;
using Microsoft.DotNet.Openapi.Tools;
using Microsoft.DotNet.Openapi.Tools.Internal;
using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.DotNet.OpenApi.Commands
{
internal abstract class BaseCommand : CommandLineApplication
{
protected string WorkingDirectory;
protected readonly IHttpClientWrapper _httpClient;
public const string OpenApiReference = "OpenApiReference";
public const string OpenApiProjectReference = "OpenApiProjectReference";
protected const string SourceUrlAttrName = "SourceUrl";
public const string ContentDispositionHeaderName = "Content-Disposition";
private const string CodeGeneratorAttrName = "CodeGenerator";
private const string DefaultExtension = ".json";
internal const string PackageVersionUrl = "https://go.microsoft.com/fwlink/?linkid=2099561";
public BaseCommand(CommandLineApplication parent, string name, IHttpClientWrapper httpClient)
{
Parent = parent;
Name = name;
Out = parent.Out ?? Out;
Error = parent.Error ?? Error;
_httpClient = httpClient;
ProjectFileOption = Option("-p|--updateProject", "The project file update.", CommandOptionType.SingleValue);
if (Parent is Application)
{
WorkingDirectory = ((Application)Parent).WorkingDirectory;
}
else
{
WorkingDirectory = ((Application)Parent.Parent).WorkingDirectory;
}
OnExecute(ExecuteAsync);
}
public CommandOption ProjectFileOption { get; }
public TextWriter Warning
{
get { return Out; }
}
protected abstract Task<int> ExecuteCoreAsync();
protected abstract bool ValidateArguments();
private async Task<int> ExecuteAsync()
{
if (GetApplication().Help.HasValue())
{
ShowHelp();
return 0;
}
if (!ValidateArguments())
{
ShowHelp();
return 1;
}
return await ExecuteCoreAsync();
}
private Application GetApplication()
{
var parent = Parent;
while(!(parent is Application))
{
parent = parent.Parent;
}
return (Application)parent;
}
internal FileInfo ResolveProjectFile(CommandOption projectOption)
{
string project;
if (projectOption.HasValue())
{
project = projectOption.Value();
project = GetFullPath(project);
if (!File.Exists(project))
{
throw new ArgumentException($"The project '{project}' does not exist.");
}
}
else
{
var projects = Directory.GetFiles(WorkingDirectory, "*.csproj", SearchOption.TopDirectoryOnly);
if (projects.Length == 0)
{
throw new ArgumentException("No project files were found in the current directory. Either move to a new directory or provide the project explicitly");
}
if (projects.Length > 1)
{
throw new ArgumentException("More than one project was found in this directory, either remove a duplicate or explicitly provide the project.");
}
project = projects[0];
}
return new FileInfo(project);
}
protected Project LoadProject(FileInfo projectFile)
{
var project = ProjectCollection.GlobalProjectCollection.LoadProject(
projectFile.FullName,
globalProperties: null,
toolsVersion: null);
project.ReevaluateIfNecessary();
return project;
}
internal bool IsProjectFile(string file)
{
return File.Exists(Path.GetFullPath(file)) && file.EndsWith(".csproj");
}
internal bool IsUrl(string file)
{
return Uri.TryCreate(file, UriKind.Absolute, out var _) && file.StartsWith("http");
}
internal async Task AddOpenAPIReference(
string tagName,
FileInfo projectFile,
string sourceFile,
CodeGenerator? codeGenerator,
string sourceUrl = null)
{
// EnsurePackagesInProjectAsync MUST happen before LoadProject, because otherwise the global state set by ProjectCollection doesn't pick up the nuget edits, and we end up losing them.
await EnsurePackagesInProjectAsync(projectFile, codeGenerator);
var project = LoadProject(projectFile);
var items = project.GetItems(tagName);
var fileItems = items.Where(i => string.Equals(GetFullPath(i.EvaluatedInclude), GetFullPath(sourceFile), StringComparison.Ordinal));
if (fileItems.Count() > 0)
{
Warning.Write($"One or more references to {sourceFile} already exist in '{project.FullPath}'. Duplicate references could lead to unexpected behavior.");
return;
}
if (sourceUrl != null)
{
if (items.Any(i => string.Equals(i.GetMetadataValue(SourceUrlAttrName), sourceUrl)))
{
Warning.Write($"A reference to '{sourceUrl}' already exists in '{project.FullPath}'.");
return;
}
}
var metadata = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(sourceUrl))
{
metadata[SourceUrlAttrName] = sourceUrl;
}
if (codeGenerator != null)
{
metadata[CodeGeneratorAttrName] = codeGenerator.ToString();
}
project.AddElementWithAttributes(tagName, sourceFile, metadata);
project.Save();
}
private async Task EnsurePackagesInProjectAsync(FileInfo projectFile, CodeGenerator? codeGenerator)
{
var urlPackages = await LoadPackageVersionsFromURLAsync();
var attributePackages = GetServicePackages(codeGenerator);
foreach (var kvp in attributePackages)
{
var packageId = kvp.Key;
var version = urlPackages != null && urlPackages.ContainsKey(packageId) ? urlPackages[packageId] : kvp.Value;
var args = new[] {
"add",
"package",
packageId,
"--version",
version,
"--no-restore"
};
var muxer = DotNetMuxer.MuxerPathOrDefault();
if (string.IsNullOrEmpty(muxer))
{
throw new ArgumentException($"dotnet was not found on the path.");
}
var startInfo = new ProcessStartInfo
{
FileName = muxer,
Arguments = string.Join(" ", args),
WorkingDirectory = projectFile.Directory.FullName,
RedirectStandardError = true,
RedirectStandardOutput = true,
};
var process = Process.Start(startInfo);
var timeout = 20;
if (!process.WaitForExit(timeout * 1000))
{
throw new ArgumentException($"Adding package `{packageId}` to `{projectFile.Directory}` took longer than {timeout} seconds.");
}
if (process.ExitCode != 0)
{
Out.Write(process.StandardOutput.ReadToEnd());
Error.Write(process.StandardError.ReadToEnd());
throw new ArgumentException($"Could not add package `{packageId}` to `{projectFile.Directory}`");
}
}
}
internal async Task DownloadToFileAsync(string url, string destinationPath, bool overwrite)
{
using var response = await _httpClient.GetResponseAsync(url);
await WriteToFileAsync(await response.Stream, destinationPath, overwrite);
}
internal async Task<string> DownloadGivenOption(string url, CommandOption fileOption)
{
using var response = await _httpClient.GetResponseAsync(url);
if (response.IsSuccessCode())
{
string destinationPath;
if (fileOption.HasValue())
{
destinationPath = fileOption.Value();
}
else
{
var fileName = GetFileNameFromResponse(response, url);
var fullPath = GetFullPath(fileName);
var directory = Path.GetDirectoryName(fullPath);
destinationPath = GetUniqueFileName(directory, Path.GetFileNameWithoutExtension(fileName), Path.GetExtension(fileName));
}
await WriteToFileAsync(await response.Stream, GetFullPath(destinationPath), overwrite: false);
return destinationPath;
}
else
{
throw new ArgumentException($"The given url returned '{response.StatusCode}', indicating failure. The url might be wrong, or there might be a networking issue.");
}
}
private string GetUniqueFileName(string directory, string fileName, string extension)
{
var uniqueName = fileName;
var filePath = Path.Combine(directory, fileName + extension);
var exists = true;
var count = 0;
do
{
if (!File.Exists(filePath))
{
exists = false;
}
else
{
count++;
uniqueName = fileName + count;
filePath = Path.Combine(directory, uniqueName + extension);
}
}
while (exists);
return uniqueName + extension;
}
private string GetFileNameFromResponse(IHttpResponseMessageWrapper response, string url)
{
var contentDisposition = response.ContentDisposition();
string result;
if (contentDisposition != null && contentDisposition.FileName != null)
{
var fileName = Path.GetFileName(contentDisposition.FileName);
if (!Path.HasExtension(fileName))
{
fileName += DefaultExtension;
}
result = fileName;
}
else
{
var uri = new Uri(url);
if (uri.Segments.Count() > 0 && uri.Segments.Last() != "/")
{
var lastSegment = uri.Segments.Last();
if (!Path.HasExtension(lastSegment))
{
lastSegment += DefaultExtension;
}
result = lastSegment;
}
else
{
var parts = uri.Host.Split('.');
// There's no segment, use the domain name.
string domain;
switch (parts.Length)
{
case 1:
case 2:
// It's localhost if 1, no www if 2
domain = parts.First();
break;
case 3:
domain = parts[1];
break;
default:
throw new NotImplementedException("We don't handle the case that the Host has more than three segments");
}
result = domain + DefaultExtension;
}
}
return result;
}
internal CodeGenerator? GetCodeGenerator(CommandOption codeGeneratorOption)
{
CodeGenerator? codeGenerator;
if (codeGeneratorOption.HasValue())
{
codeGenerator = Enum.Parse<CodeGenerator>(codeGeneratorOption.Value());
}
else
{
codeGenerator = null;
}
return codeGenerator;
}
internal void ValidateCodeGenerator(CommandOption codeGeneratorOption)
{
if (codeGeneratorOption.HasValue())
{
var value = codeGeneratorOption.Value();
if (!Enum.TryParse(value, out CodeGenerator _))
{
throw new ArgumentException($"Invalid value '{value}' given as code generator.");
}
}
}
internal string GetFullPath(string path)
{
return Path.IsPathFullyQualified(path)
? path
: Path.GetFullPath(path, WorkingDirectory);
}
private async Task<IDictionary<string, string>> LoadPackageVersionsFromURLAsync()
{
/* Example Json content
{
"Version" : "1.0",
"Packages" : {
"Microsoft.Azure.SignalR": "1.1.0-preview1-10442",
"Grpc.AspNetCore.Server": "0.1.22-pre2",
"Grpc.Net.ClientFactory": "0.1.22-pre2",
"Google.Protobuf": "3.8.0",
"Grpc.Tools": "1.22.0",
"NSwag.ApiDescription.Client": "13.0.3",
"Microsoft.Extensions.ApiDescription.Client": "0.3.0-preview7.19365.7",
"Newtonsoft.Json": "12.0.2"
}
}*/
try
{
using var packageVersionStream = await (await _httpClient.GetResponseAsync(PackageVersionUrl)).Stream;
using var packageVersionDocument = await JsonDocument.ParseAsync(packageVersionStream);
var packageVersionsElement = packageVersionDocument.RootElement.GetProperty("Packages");
var packageVersionsDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var packageVersion in packageVersionsElement.EnumerateObject())
{
packageVersionsDictionary[packageVersion.Name] = packageVersion.Value.GetString();
}
return packageVersionsDictionary;
}
catch
{
// TODO (johluo): Consider logging a message indicating what went wrong and actions, if any, to be taken to resolve possible issues.
// Currently not logging anything since the fwlink is not published yet.
return null;
}
}
private static IDictionary<string, string> GetServicePackages(CodeGenerator? type)
{
CodeGenerator generator = type ?? CodeGenerator.NSwagCSharp;
var name = Enum.GetName(typeof(CodeGenerator), generator);
var attributes = typeof(Program).Assembly.GetCustomAttributes<OpenApiDependencyAttribute>();
var packages = attributes.Where(a => a.CodeGenerators.Contains(generator));
var result = new Dictionary<string, string>();
if (packages != null)
{
foreach (var package in packages)
{
result[package.Name] = package.Version;
}
}
return result;
}
private static byte[] GetHash(Stream stream)
{
SHA256 algorithm;
try
{
algorithm = SHA256.Create();
}
catch (TargetInvocationException)
{
// SHA256.Create is documented to throw this exception on FIPS-compliant machines. See
// https://msdn.microsoft.com/en-us/library/z08hz7ad Fall back to a FIPS-compliant SHA256 algorithm.
algorithm = new SHA256CryptoServiceProvider();
}
using (algorithm)
{
return algorithm.ComputeHash(stream);
}
}
private async Task WriteToFileAsync(Stream content, string destinationPath, bool overwrite)
{
if (content.CanSeek)
{
content.Seek(0, SeekOrigin.Begin);
}
destinationPath = GetFullPath(destinationPath);
var destinationExists = File.Exists(destinationPath);
if (destinationExists && !overwrite)
{
throw new ArgumentException($"File '{destinationPath}' already exists. Aborting to avoid conflicts. Provide the '--output-file' argument with an unused file to resolve.");
}
await Out.WriteLineAsync($"Downloading to '{destinationPath}'.");
var reachedCopy = false;
try
{
if (destinationExists)
{
// Check hashes before using the downloaded information.
var downloadHash = GetHash(content);
byte[] destinationHash;
using (var destinationStream = File.OpenRead(destinationPath))
{
destinationHash = GetHash(destinationStream);
}
var sameHashes = downloadHash.Length == destinationHash.Length;
for (var i = 0; sameHashes && i < downloadHash.Length; i++)
{
sameHashes = downloadHash[i] == destinationHash[i];
}
if (sameHashes)
{
await Out.WriteLineAsync($"Not overwriting existing and matching file '{destinationPath}'.");
return;
}
}
else
{
// May need to create directory to hold the file.
var destinationDirectory = Path.GetDirectoryName(destinationPath);
if (!string.IsNullOrEmpty(destinationDirectory) && !Directory.Exists(destinationDirectory))
{
Directory.CreateDirectory(destinationDirectory);
}
}
// Create or overwrite the destination file.
reachedCopy = true;
using var fileStream = new FileStream(destinationPath, FileMode.OpenOrCreate, FileAccess.Write);
fileStream.Seek(0, SeekOrigin.Begin);
if (content.CanSeek)
{
content.Seek(0, SeekOrigin.Begin);
}
await content.CopyToAsync(fileStream);
}
catch (Exception ex)
{
await Error.WriteLineAsync($"Downloading failed.");
await Error.WriteLineAsync(ex.ToString());
if (reachedCopy)
{
File.Delete(destinationPath);
}
}
}
}
}

View File

@ -0,0 +1,67 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Build.Evaluation;
using Microsoft.DotNet.Openapi.Tools;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.OpenApi.Commands
{
internal class RefreshCommand : BaseCommand
{
private const string CommandName = "refresh";
private const string SourceURLArgName = "source-URL";
public RefreshCommand(Application parent, IHttpClientWrapper httpClient) : base(parent, CommandName, httpClient)
{
_sourceFileArg = Argument(SourceURLArgName, $"The OpenAPI reference to refresh.");
}
internal readonly CommandArgument _sourceFileArg;
protected override async Task<int> ExecuteCoreAsync()
{
var projectFile = ResolveProjectFile(ProjectFileOption);
var sourceFile = Ensure.NotNullOrEmpty(_sourceFileArg.Value, SourceURLArgName);
var destination = FindReferenceFromUrl(projectFile, sourceFile);
await DownloadToFileAsync(sourceFile, destination, overwrite: true);
return 0;
}
private string FindReferenceFromUrl(FileInfo projectFile, string url)
{
var project = LoadProject(projectFile);
var openApiReferenceItems = project.GetItems(OpenApiReference);
foreach (ProjectItem item in openApiReferenceItems)
{
var attrUrl = item.GetMetadataValue(SourceUrlAttrName);
if (string.Equals(attrUrl, url, StringComparison.Ordinal))
{
return item.EvaluatedInclude;
}
}
throw new ArgumentException("There was no OpenAPI reference to refresh with the given URL.");
}
protected override bool ValidateArguments()
{
var sourceFile = Ensure.NotNullOrEmpty(_sourceFileArg.Value, SourceURLArgName);
if (!IsUrl(sourceFile))
{
throw new ArgumentException($"'dotnet openapi refresh' must be given a URL");
}
return true;
}
}
}

View File

@ -0,0 +1,78 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Build.Evaluation;
using Microsoft.DotNet.Openapi.Tools;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.OpenApi.Commands
{
internal class RemoveCommand : BaseCommand
{
private const string CommandName = "remove";
private const string SourceArgName = "soruce";
public RemoveCommand(Application parent, IHttpClientWrapper httpClient) : base(parent, CommandName, httpClient)
{
_sourceProjectArg = Argument(SourceArgName, $"The OpenAPI reference to remove. Must represent a reference which is already in this project", multipleValues: true);
}
internal readonly CommandArgument _sourceProjectArg;
protected override Task<int> ExecuteCoreAsync()
{
var projectFile = ResolveProjectFile(ProjectFileOption);
var sourceFile = Ensure.NotNullOrEmpty(_sourceProjectArg.Value, SourceArgName);
if (IsProjectFile(sourceFile))
{
RemoveServiceReference(OpenApiProjectReference, projectFile, sourceFile);
}
else
{
var file = RemoveServiceReference(OpenApiReference, projectFile, sourceFile);
if (file != null)
{
File.Delete(GetFullPath(file));
}
}
return Task.FromResult(0);
}
private string RemoveServiceReference(string tagName, FileInfo projectFile, string sourceFile)
{
var project = LoadProject(projectFile);
var openApiReferenceItems = project.GetItems(tagName);
foreach (ProjectItem item in openApiReferenceItems)
{
var include = item.EvaluatedInclude;
var sourceUrl = item.HasMetadata(SourceUrlAttrName) ? item.GetMetadataValue(SourceUrlAttrName) : null;
if (string.Equals(include, sourceFile, StringComparison.OrdinalIgnoreCase)
|| string.Equals(sourceUrl, sourceFile, StringComparison.OrdinalIgnoreCase))
{
project.RemoveItem(item);
project.Save();
return include;
}
}
Warning.Write($"No OpenAPI reference was found with the file '{sourceFile}'");
return null;
}
protected override bool ValidateArguments()
{
Ensure.NotNullOrEmpty(_sourceProjectArg.Value, SourceArgName);
return true;
}
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
namespace Microsoft.DotNet.OpenApi
{
internal static class DebugMode
{
public static void HandleDebugSwitch(ref string[] args)
{
if (args.Length > 0 && string.Equals("--debug", args[0], StringComparison.OrdinalIgnoreCase))
{
args = args.Skip(1).ToArray();
Console.WriteLine("Waiting for debugger in pid: {0}", Process.GetCurrentProcess().Id);
while (!Debugger.IsAttached)
{
Thread.Sleep(TimeSpan.FromSeconds(3));
}
}
}
}
}

View File

@ -0,0 +1,72 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.DotNet.OpenApi;
using Microsoft.DotNet.OpenApi.Commands;
namespace Microsoft.DotNet.Openapi.Tools
{
public class HttpClientWrapper : IHttpClientWrapper
{
private readonly HttpClient _client;
public HttpClientWrapper(HttpClient client)
{
_client = client;
}
public void Dispose()
{
_client.Dispose();
}
public async Task<IHttpResponseMessageWrapper> GetResponseAsync(string url)
{
var response = await _client.GetAsync(url);
return new HttpResponseMessageWrapper(response);
}
public Task<Stream> GetStreamAsync(string url)
{
return _client.GetStreamAsync(url);
}
}
public class HttpResponseMessageWrapper : IHttpResponseMessageWrapper
{
private HttpResponseMessage _response;
public HttpResponseMessageWrapper(HttpResponseMessage response)
{
_response = response;
}
public Task<Stream> Stream => _response.Content.ReadAsStreamAsync();
public HttpStatusCode StatusCode => _response.StatusCode;
public bool IsSuccessCode() => _response.IsSuccessStatusCode;
public ContentDispositionHeaderValue ContentDisposition()
{
if (_response.Headers.TryGetValues(BaseCommand.ContentDispositionHeaderName, out var disposition))
{
return new ContentDispositionHeaderValue(disposition.First());
}
return null;
}
public void Dispose()
{
_response.Dispose();
}
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.DotNet.OpenApi;
namespace Microsoft.DotNet.Openapi.Tools
{
internal interface IHttpClientWrapper : IDisposable
{
Task<IHttpResponseMessageWrapper> GetResponseAsync(string url);
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Net;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace Microsoft.DotNet.OpenApi
{
public interface IHttpResponseMessageWrapper : IDisposable
{
Task<Stream> Stream { get; }
ContentDispositionHeaderValue ContentDisposition();
HttpStatusCode StatusCode { get; }
bool IsSuccessCode();
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.DotNet.OpenApi;
namespace Microsoft.DotNet.Openapi.Tools.Internal
{
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
internal class OpenApiDependencyAttribute : Attribute
{
public OpenApiDependencyAttribute(string name, string version, string codeGenerators)
{
Name = name;
Version = version;
CodeGenerators = codeGenerators.Split(';', StringSplitOptions.RemoveEmptyEntries).Select(c => Enum.Parse<CodeGenerator>(c)).ToArray();
}
public string Name { get; set; }
public string Version { get; set; }
public IEnumerable<CodeGenerator> CodeGenerators { get; set; }
}
}

View File

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<OutputType>exe</OutputType>
<Description>Command line tool to add an OpenAPI service reference</Description>
<RootNamespace>Microsoft.DotNet.Openapi.Tools</RootNamespace>
<AssemblyName>dotnet-openapi</AssemblyName>
<PackageId>Microsoft.dotnet-openapi</PackageId>
<PackAsTool>true</PackAsTool>
<!-- This package is for internal use only. It contains a CLI tool. -->
<IsShippingPackage>false</IsShippingPackage>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(ToolSharedSourceRoot)CommandLine\**\*.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Build" ExcludeAssets="runtime" />
<Reference Include="Microsoft.Build.Locator" />
<Reference Include="Microsoft.Extensions.CommandLineUtils.Sources" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="Microsoft.DotNet.Openapi.Tools.Internal.OpenApiDependencyAttribute">
<_Parameter1>NSwag.ApiDescription.Client</_Parameter1>
<_Parameter2>$(NSwagApiDescriptionClientPackageVersion)</_Parameter2>
<_Parameter3>NSwagCSharp;NSwagTypeScript</_Parameter3>
</AssemblyAttribute>
<AssemblyAttribute Include="Microsoft.DotNet.Openapi.Tools.Internal.OpenApiDependencyAttribute">
<_Parameter1>Newtonsoft.Json</_Parameter1>
<_Parameter2>$(NewtonsoftJsonPackageVersion)</_Parameter2>
<_Parameter3>NSwagCSharp;NSwagTypeScript</_Parameter3>
</AssemblyAttribute>
</ItemGroup>
</Project>

View File

@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Net.Http;
using Microsoft.DotNet.Openapi.Tools;
namespace Microsoft.DotNet.OpenApi
{
public class Program
{
public static int Main(string[] args)
{
var outputWriter = new StringWriter();
var errorWriter = new StringWriter();
DebugMode.HandleDebugSwitch(ref args);
try
{
using var httpClient = new HttpClientWrapper(new HttpClient());
var application = new Application(
Directory.GetCurrentDirectory(),
httpClient,
outputWriter,
errorWriter);
var result = application.Execute(args);
return result;
}
catch (Exception ex)
{
errorWriter.Write("Unexpected error:");
errorWriter.WriteLine(ex.ToString());
}
finally
{
var output = outputWriter.ToString();
var error = errorWriter.ToString();
outputWriter.Dispose();
errorWriter.Dispose();
Console.WriteLine(output);
Console.Error.WriteLine(error);
}
return 1;
}
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.Build.Evaluation;
namespace Microsoft.DotNet.OpenApi
{
public static class ProjectExtensions
{
public static void AddElementWithAttributes(this Project project, string tagName, string include, IDictionary<string, string> metadata)
{
var item = project.AddItem(tagName, include).Single();
foreach (var kvp in metadata)
{
item.Xml.AddMetadata(kvp.Key, kvp.Value, expressAsAttribute: true);
}
project.Save();
}
}
}

View File

@ -0,0 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.DotNet.Open.Api.Tools.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -0,0 +1,254 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.DotNet.OpenApi.Tests;
using Microsoft.Extensions.Internal;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.DotNet.OpenApi.Add.Tests
{
public class OpenApiAddFileTests : OpenApiTestBase
{
public OpenApiAddFileTests(ITestOutputHelper output) : base(output) { }
[Fact]
public void OpenApi_Empty_ShowsHelp()
{
var app = GetApplication();
var run = app.Execute(new string[] { });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
Assert.Contains("Usage: openapi ", _output.ToString());
}
[Fact]
public void OpenApi_NoProjectExists()
{
var app = GetApplication();
_tempDir.Create();
var run = app.Execute(new string[] { "add", "file", "randomfile.json" });
Assert.Contains("No project files were found in the current directory", _error.ToString());
Assert.Equal(1, run);
}
[Fact]
public void OpenApi_ExplicitProject_Missing()
{
var app = GetApplication();
_tempDir.Create();
var csproj = "fake.csproj";
var run = app.Execute(new string[] { "add", "file", "--updateProject", csproj, "randomfile.json" });
Assert.Contains($"The project '{Path.Combine(_tempDir.Root, csproj)}' does not exist.", _error.ToString());
Assert.Equal(1, run);
}
[Fact]
public void OpenApi_Add_Empty_ShowsHelp()
{
var app = GetApplication();
var run = app.Execute(new string[] { "add" });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
Assert.Contains("Usage: openapi add", _output.ToString());
}
[Fact]
public void OpenApi_Add_File_Empty_ShowsHelp()
{
var app = GetApplication();
var run = app.Execute(new string[] { "add", "file", "--help" });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
Assert.Contains("Usage: openapi ", _output.ToString());
}
[Fact]
public async Task OpenApi_Add_ReuseItemGroup()
{
var project = CreateBasicProject(withOpenApi: true);
var app = GetApplication();
var run = app.Execute(new[] { "add", "file", project.NSwagJsonFile });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var secondRun = app.Execute(new[] { "add", "url", FakeOpenApiUrl });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, secondRun);
var csproj = new FileInfo(project.Project.Path);
string content;
using (var csprojStream = csproj.OpenRead())
using (var reader = new StreamReader(csprojStream))
{
content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains($"<OpenApiReference Include=\"{project.NSwagJsonFile}\"", content);
}
var projXml = new XmlDocument();
projXml.Load(csproj.FullName);
var openApiRefs = projXml.GetElementsByTagName(Commands.BaseCommand.OpenApiReference);
Assert.Same(openApiRefs[0].ParentNode, openApiRefs[1].ParentNode);
}
[Fact]
public void OpenApi_Add_File_EquivilentPaths()
{
var project = CreateBasicProject(withOpenApi: true);
var nswagJsonFile = project.NSwagJsonFile;
var app = GetApplication();
var run = app.Execute(new[] { "add", "file", nswagJsonFile });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
app = GetApplication();
var absolute = Path.GetFullPath(nswagJsonFile, project.Project.Dir().Root);
run = app.Execute(new[] { "add", "file", absolute });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var csproj = new FileInfo(project.Project.Path);
var projXml = new XmlDocument();
projXml.Load(csproj.FullName);
var openApiRefs = projXml.GetElementsByTagName(Commands.BaseCommand.OpenApiReference);
Assert.Single(openApiRefs);
}
[Fact]
public async Task OpenApi_Add_NSwagTypeScript()
{
var project = CreateBasicProject(withOpenApi: true);
var nswagJsonFile = project.NSwagJsonFile;
var app = GetApplication();
var run = app.Execute(new[] { "add", "file", nswagJsonFile, "--code-generator", "NSwagTypeScript" });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
// csproj contents
var csproj = new FileInfo(project.Project.Path);
using (var csprojStream = csproj.OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains($"<OpenApiReference Include=\"{nswagJsonFile}\" CodeGenerator=\"NSwagTypeScript\" />", content);
}
// Build project and make sure it compiles
using var buildProc = ProcessEx.Run(_outputHelper, _tempDir.Root, "dotnet", "build");
await buildProc.Exited;
Assert.True(buildProc.ExitCode == 0, $"Build failed: {buildProc.Output}");
// Run project and make sure it doesn't crash
using var runProc = ProcessEx.Run(_outputHelper, _tempDir.Root, "dotnet", "run");
Thread.Sleep(100);
Assert.False(runProc.HasExited, $"Run failed with: {runProc.Output}");
}
[Fact]
public async Task OpenApi_Add_FromJson()
{
var project = CreateBasicProject(withOpenApi: true);
var nswagJsonFile = project.NSwagJsonFile;
var app = GetApplication();
var run = app.Execute(new[] { "add", "file", nswagJsonFile });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
// csproj contents
var csproj = new FileInfo(project.Project.Path);
using (var csprojStream = csproj.OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains($"<OpenApiReference Include=\"{nswagJsonFile}\"", content);
}
// Build project and make sure it compiles
var buildProc = ProcessEx.Run(_outputHelper, _tempDir.Root, "dotnet", "build");
await buildProc.Exited;
Assert.True(buildProc.ExitCode == 0, $"Build failed: {buildProc.Output}");
// Run project and make sure it doesn't crash
using var runProc = ProcessEx.Run(_outputHelper, _tempDir.Root, "dotnet", "run");
Thread.Sleep(100);
Assert.False(runProc.HasExited, $"Run failed with: {runProc.Output}");
}
[Fact]
public async Task OpenApi_Add_File_UseProjectOption()
{
var project = CreateBasicProject(withOpenApi: true);
var nswagJsonFIle = project.NSwagJsonFile;
var app = GetApplication();
var run = app.Execute(new[] { "add", "file", "--updateProject", project.Project.Path, nswagJsonFIle });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
// csproj contents
var csproj = new FileInfo(project.Project.Path);
using var csprojStream = csproj.OpenRead();
using var reader = new StreamReader(csprojStream);
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains($"<OpenApiReference Include=\"{nswagJsonFIle}\"", content);
}
[Fact]
public async Task OpenApi_Add_MultipleTimes_OnlyOneReference()
{
var project = CreateBasicProject(withOpenApi: true);
var nswagJsonFile = project.NSwagJsonFile;
var app = GetApplication();
var run = app.Execute(new[] { "add", "file", nswagJsonFile });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
app = GetApplication();
run = app.Execute(new[] { "add", "file", nswagJsonFile });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
// csproj contents
var csproj = new FileInfo(project.Project.Path);
using var csprojStream = csproj.OpenRead();
using var reader = new StreamReader(csprojStream);
var content = await reader.ReadToEndAsync();
var escapedPkgRef = Regex.Escape("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"");
Assert.Single(Regex.Matches(content, escapedPkgRef));
var escapedApiRef = Regex.Escape($"<OpenApiReference Include=\"{nswagJsonFile}\"");
Assert.Single(Regex.Matches(content, escapedApiRef));
}
}
}

View File

@ -0,0 +1,123 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.DotNet.OpenApi.Tests;
using Microsoft.Extensions.Tools.Internal;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.DotNet.OpenApi.Add.Tests
{
public class OpenApiAddProjectTests : OpenApiTestBase
{
public OpenApiAddProjectTests(ITestOutputHelper output) : base(output){}
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12738")]
public async Task OpenApi_Add_GlobbingOpenApi()
{
var project = CreateBasicProject(withOpenApi: true);
using (var refProj1 = project.Project.Dir().SubDir("refProj1"))
using (var refProj2 = project.Project.Dir().SubDir("refProj2"))
{
var project1 = refProj1.WithCSharpProject("refProj");
project1
.WithTargetFrameworks("netcoreapp3.0")
.Dir()
.Create();
var project2 = refProj2.WithCSharpProject("refProj2");
project2
.WithTargetFrameworks("netcoreapp3.0")
.Dir()
.Create();
var app = GetApplication();
var run = app.Execute(new[] { "add", "project", project1.Path, project2.Path});
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
// csproj contents
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains($"<OpenApiProjectReference Include=\"{project1.Path}\"", content);
Assert.Contains($"<OpenApiProjectReference Include=\"{project2.Path}\"", content);
}
}
}
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12738")]
public void OpenApi_Add_Project_EquivilentPaths()
{
var project = CreateBasicProject(withOpenApi: false);
using (var refProj = new TemporaryDirectory())
{
var refProjName = "refProj";
var csproj = refProj.WithCSharpProject(refProjName);
csproj
.WithTargetFrameworks("netcoreapp3.0")
.Dir()
.Create();
var app = GetApplication();
var run = app.Execute(new[] { "add", "project", csproj.Path});
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
app = GetApplication();
run = app.Execute(new[] { "add", "project", Path.Combine(csproj.Path, "..", "refProj.csproj")});
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var projXml = new XmlDocument();
projXml.Load(project.Project.Path);
var openApiRefs = projXml.GetElementsByTagName(Commands.BaseCommand.OpenApiProjectReference);
Assert.Single(openApiRefs);
}
}
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12738")]
public async Task OpenApi_Add_FromCsProj()
{
var project = CreateBasicProject(withOpenApi: false);
using (var refProj = new TemporaryDirectory())
{
var refProjName = "refProj";
refProj
.WithCSharpProject(refProjName)
.WithTargetFrameworks("netcoreapp3.0")
.Dir()
.Create();
var app = GetApplication();
var refProjFile = Path.Join(refProj.Root, $"{refProjName}.csproj");
var run = app.Execute(new[] { "add", "project", refProjFile });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
// csproj contents
using(var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using(var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains($"<OpenApiProjectReference Include=\"{refProjFile}\"", content);
}
}
}
}
}

View File

@ -0,0 +1,493 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.DotNet.OpenApi.Tests;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.DotNet.OpenApi.Add.Tests
{
public class OpenApiAddURLTests : OpenApiTestBase
{
public OpenApiAddURLTests(ITestOutputHelper output) : base(output){ }
[Fact]
public async Task OpenApi_Add_Url_WithContentDisposition()
{
var project = CreateBasicProject(withOpenApi: false);
var app = GetApplication();
var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var expectedJsonName = "filename.json";
// csproj contents
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains(
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" />", content);
}
var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName);
Assert.True(File.Exists(jsonFile));
using (var jsonStream = new FileInfo(jsonFile).OpenRead())
using (var reader = new StreamReader(jsonStream))
{
var content = await reader.ReadToEndAsync();
Assert.Equal(Content, content);
}
}
[Fact]
public async Task OpenAPI_Add_Url_NoContentDisposition()
{
var project = CreateBasicProject(withOpenApi: false);
var url = NoDispositionUrl;
var app = GetApplication();
var run = app.Execute(new[] { "add", "url", url});
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var expectedJsonName = "nodisposition.yaml";
// csproj contents
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains(
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{url}"" />", content);
}
var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName);
Assert.True(File.Exists(jsonFile));
using (var jsonStream = new FileInfo(jsonFile).OpenRead())
using (var reader = new StreamReader(jsonStream))
{
var content = await reader.ReadToEndAsync();
Assert.Equal(Content, content);
}
}
[Fact]
public async Task OpenAPI_Add_Url_NoExtension_AssumesJson()
{
var project = CreateBasicProject(withOpenApi: false);
var url = NoExtensionUrl;
var app = GetApplication();
var run = app.Execute(new[] { "add", "url", url });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var expectedJsonName = "filename.json";
// csproj contents
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains(
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{url}"" />", content);
}
var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName);
Assert.True(File.Exists(jsonFile));
using (var jsonStream = new FileInfo(jsonFile).OpenRead())
using (var reader = new StreamReader(jsonStream))
{
var content = await reader.ReadToEndAsync();
Assert.Equal(Content, content);
}
}
[Fact]
public async Task OpenApi_Add_Url_NoSegment()
{
var project = CreateBasicProject(withOpenApi: false);
var url = NoSegmentUrl;
var app = GetApplication();
var run = app.Execute(new[] { "add", "url", url });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var expectedJsonName = "contoso.json";
// csproj contents
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains(
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{url}"" />", content);
}
var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName);
Assert.True(File.Exists(jsonFile));
using (var jsonStream = new FileInfo(jsonFile).OpenRead())
using (var reader = new StreamReader(jsonStream))
{
var content = await reader.ReadToEndAsync();
Assert.Equal(Content, content);
}
}
[Fact]
public async Task OpenApi_Add_Url()
{
var project = CreateBasicProject(withOpenApi: false);
var app = GetApplication();
var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var expectedJsonName = "filename.json";
// csproj contents
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains(
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" />", content);
}
var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName);
Assert.True(File.Exists(jsonFile));
using (var jsonStream = new FileInfo(jsonFile).OpenRead())
using (var reader = new StreamReader(jsonStream))
{
var content = await reader.ReadToEndAsync();
Assert.Equal(Content, content);
}
}
[Fact]
public async Task OpenApi_Add_Url_SameName_UniqueFile()
{
var project = CreateBasicProject(withOpenApi: false);
var app = GetApplication();
var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var firstExpectedJsonName = "filename.json";
// csproj contents
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains(
$@"<OpenApiReference Include=""{firstExpectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" />", content);
}
var firstJsonFile = Path.Combine(_tempDir.Root, firstExpectedJsonName);
Assert.True(File.Exists(firstJsonFile));
using (var jsonStream = new FileInfo(firstJsonFile).OpenRead())
using (var reader = new StreamReader(jsonStream))
{
var content = await reader.ReadToEndAsync();
Assert.Equal(Content, content);
}
app = GetApplication();
run = app.Execute(new[] { "add", "url", NoExtensionUrl });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var secondExpectedJsonName = "filename1.json";
// csproj contents
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains(
$@"<OpenApiReference Include=""{firstExpectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" />", content);
Assert.Contains(
$@"<OpenApiReference Include=""{secondExpectedJsonName}"" SourceUrl=""{NoExtensionUrl}"" />", content);
}
var secondJsonFile = Path.Combine(_tempDir.Root, secondExpectedJsonName);
Assert.True(File.Exists(secondJsonFile));
using (var jsonStream = new FileInfo(secondJsonFile).OpenRead())
using (var reader = new StreamReader(jsonStream))
{
var content = await reader.ReadToEndAsync();
Assert.Equal(Content, content);
}
}
[Fact]
public async Task OpenApi_Add_Url_NSwagCSharp()
{
var project = CreateBasicProject(withOpenApi: false);
var app = GetApplication();
var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl, "--code-generator", "NSwagCSharp" });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var expectedJsonName = "filename.json";
// csproj contents
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains(
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" CodeGenerator=""NSwagCSharp"" />", content);
}
var resultFile = Path.Combine(_tempDir.Root, expectedJsonName);
Assert.True(File.Exists(resultFile));
using (var jsonStream = new FileInfo(resultFile).OpenRead())
using (var reader = new StreamReader(jsonStream))
{
var content = await reader.ReadToEndAsync();
Assert.Equal(Content, content);
}
}
[Fact]
public async Task OpenApi_Add_Url_NSwagTypeScript()
{
var project = CreateBasicProject(withOpenApi: false);
var app = GetApplication();
var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl, "--code-generator", "NSwagTypeScript" });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var expectedJsonName = "filename.json";
// csproj contents
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains(
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" CodeGenerator=""NSwagTypeScript"" />", content);
}
var resultFile = Path.Combine(_tempDir.Root, expectedJsonName);
Assert.True(File.Exists(resultFile));
using (var jsonStream = new FileInfo(resultFile).OpenRead())
using (var reader = new StreamReader(jsonStream))
{
var content = await reader.ReadToEndAsync();
Assert.Equal(Content, content);
}
}
[Fact]
public async Task OpenApi_Add_Url_OutputFile()
{
var project = CreateBasicProject(withOpenApi: false);
var app = GetApplication();
var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl, "--output-file", Path.Combine("outputdir", "file.yaml") });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var expectedJsonName = Path.Combine("outputdir", "file.yaml");
// csproj contents
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains(
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" />", content);
}
var resultFile = Path.Combine(_tempDir.Root, expectedJsonName);
Assert.True(File.Exists(resultFile));
using (var jsonStream = new FileInfo(resultFile).OpenRead())
using (var reader = new StreamReader(jsonStream))
{
var content = await reader.ReadToEndAsync();
Assert.Equal(Content, content);
}
}
[Fact]
public async Task OpenApi_Add_URL_FileAlreadyExists_Fail()
{
var project = CreateBasicProject(withOpenApi: false);
var app = GetApplication();
var outputFile = Path.Combine("outputdir", "file.yaml");
var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl, "--output-file", outputFile });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var expectedJsonName = Path.Combine("outputdir", "file.yaml");
// csproj contents
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains(
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" />", content);
}
var resultFile = Path.Combine(_tempDir.Root, expectedJsonName);
Assert.True(File.Exists(resultFile));
using (var jsonStream = new FileInfo(resultFile).OpenRead())
using (var reader = new StreamReader(jsonStream))
{
var content = await reader.ReadToEndAsync();
Assert.Equal(Content, content);
}
// Second reference, same output
app = GetApplication();
run = app.Execute(new[] { "add", "url", DifferentUrl, "--output-file", outputFile});
Assert.Equal(1, run);
Assert.True(_error.ToString().Contains("Aborting to avoid conflicts."), $"Should have aborted to avoid conflicts");
// csproj contents
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains(
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" />", content);
Assert.DoesNotContain(
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{DifferentUrl}"" CodeGenerator=""NSwagCSharp"" />", content);
}
using (var jsonStream = new FileInfo(resultFile).OpenRead())
using (var reader = new StreamReader(jsonStream))
{
var content = await reader.ReadToEndAsync();
Assert.Equal(Content, content);
}
}
[Fact]
public void OpenApi_Add_URL_MultipleTimes_OnlyOneReference()
{
var project = CreateBasicProject(withOpenApi: false);
var app = GetApplication();
var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
app = GetApplication();
run = app.Execute(new[] { "add", "url", "--output-file", "openapi.yaml", FakeOpenApiUrl });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
// csproj contents
var csproj = new FileInfo(project.Project.Path);
using var csprojStream = csproj.OpenRead();
using var reader = new StreamReader(csprojStream);
var content = reader.ReadToEnd();
var escapedPkgRef = Regex.Escape("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"");
Assert.Single(Regex.Matches(content, escapedPkgRef));
var escapedApiRef = Regex.Escape($"SourceUrl=\"{FakeOpenApiUrl}\"");
Assert.Single(Regex.Matches(content, escapedApiRef));
}
[Fact]
public async Task OpenAPi_Add_URL_InvalidUrl()
{
var project = CreateBasicProject(withOpenApi: false);
var app = GetApplication(realHttp: true);
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(1, run);
var expectedJsonName = "dingos.json";
// csproj contents
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.DoesNotContain("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.DoesNotContain($@"<OpenApiReference", content);
}
var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName);
Assert.False(File.Exists(jsonFile));
}
[Fact]
public void OpenApi_Add_URL_ActualResponse()
{
var project = CreateBasicProject(withOpenApi: false);
var app = GetApplication(realHttp: true);
var url = ActualUrl;
var run = app.Execute(new[] { "add", "url", url });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
app = GetApplication(realHttp: true);
run = app.Execute(new[] { "add", "url", url });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
// csproj contents
var csproj = new FileInfo(project.Project.Path);
using var csprojStream = csproj.OpenRead();
using var reader = new StreamReader(csprojStream);
var content = reader.ReadToEnd();
var escapedPkgRef = Regex.Escape("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"");
Assert.Single(Regex.Matches(content, escapedPkgRef));
var escapedApiRef = Regex.Escape($"SourceUrl=\"{url}\"");
Assert.Single(Regex.Matches(content, escapedApiRef));
Assert.Contains(
$@"<OpenApiReference Include=""api-with-examples.yaml"" SourceUrl=""{ActualUrl}"" />", content);
}
}
}

View File

@ -0,0 +1,48 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.OpenApi.Tests;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.DotNet.OpenApi.Refresh.Tests
{
public class OpenApiRefreshTests : OpenApiTestBase
{
public OpenApiRefreshTests(ITestOutputHelper output) : base(output) { }
[Fact]
public async Task OpenApi_Refresh_Basic()
{
CreateBasicProject(withOpenApi: false);
var app = GetApplication();
var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var expectedJsonPath = Path.Combine(_tempDir.Root, "filename.json");
var json = await File.ReadAllTextAsync(expectedJsonPath);
json += "trash";
await File.WriteAllTextAsync(expectedJsonPath, json);
var firstWriteTime = File.GetLastWriteTime(expectedJsonPath);
Thread.Sleep(TimeSpan.FromSeconds(1));
app = GetApplication();
run = app.Execute(new[] { "refresh", FakeOpenApiUrl });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var secondWriteTime = File.GetLastWriteTime(expectedJsonPath);
Assert.True(firstWriteTime < secondWriteTime, $"File wasn't updated! {firstWriteTime} {secondWriteTime}");
}
}
}

View File

@ -0,0 +1,201 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Threading.Tasks;
using Microsoft.DotNet.OpenApi.Tests;
using Microsoft.Extensions.Tools.Internal;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.DotNet.OpenApi.Remove.Tests
{
public class OpenApiRemoveTests : OpenApiTestBase
{
public OpenApiRemoveTests(ITestOutputHelper output) : base(output) { }
[Fact]
public async Task OpenApi_Remove_File()
{
var nswagJsonFile = "openapi.json";
_tempDir
.WithCSharpProject("testproj")
.WithTargetFrameworks("netcoreapp3.0")
.Dir()
.WithContentFile(nswagJsonFile)
.WithContentFile("Startup.cs")
.Create();
var add = GetApplication();
var run = add.Execute(new[] { "add", "file", nswagJsonFile });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
// csproj contents
var csproj = new FileInfo(Path.Join(_tempDir.Root, "testproj.csproj"));
using (var csprojStream = csproj.OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains($"<OpenApiReference Include=\"{nswagJsonFile}\"", content);
}
var remove = GetApplication();
var removeRun = remove.Execute(new[] { "remove", nswagJsonFile });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, removeRun);
// csproj contents
csproj = new FileInfo(Path.Join(_tempDir.Root, "testproj.csproj"));
using (var csprojStream = csproj.OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
// Don't remove the package reference, they might have taken other dependencies on it
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.DoesNotContain($"<OpenApiReference Include=\"{nswagJsonFile}\"", content);
}
Assert.False(File.Exists(Path.Combine(_tempDir.Root, nswagJsonFile)));
}
[Fact]
public async Task OpenApi_Remove_ViaUrl()
{
_tempDir
.WithCSharpProject("testproj")
.WithTargetFrameworks("netcoreapp3.0")
.Dir()
.WithContentFile("Startup.cs")
.Create();
var add = GetApplication();
var run = add.Execute(new[] { "add", "url", FakeOpenApiUrl });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
// csproj contents
var csproj = new FileInfo(Path.Join(_tempDir.Root, "testproj.csproj"));
using (var csprojStream = csproj.OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
// Don't remove the package reference, they might have taken other dependencies on it
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
}
var remove = GetApplication();
var removeRun = remove.Execute(new[] { "remove", FakeOpenApiUrl });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, removeRun);
// csproj contents
csproj = new FileInfo(Path.Join(_tempDir.Root, "testproj.csproj"));
using var removedCsprojStream = csproj.OpenRead();
using var removedReader = new StreamReader(removedCsprojStream);
var removedContent = await removedReader.ReadToEndAsync();
// Don't remove the package reference, they might have taken other dependencies on it
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", removedContent);
Assert.DoesNotContain($"<OpenApiReference", removedContent);
}
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12738")]
public async Task OpenApi_Remove_Project()
{
_tempDir
.WithCSharpProject("testproj")
.WithTargetFrameworks("netcoreapp3.0")
.Dir()
.WithContentFile("Startup.cs")
.Create();
using var refProj = new TemporaryDirectory();
var refProjName = "refProj";
refProj
.WithCSharpProject(refProjName)
.WithTargetFrameworks("netcoreapp3.0")
.Dir()
.Create();
var app = GetApplication();
var refProjFile = Path.Join(refProj.Root, $"{refProjName}.csproj");
var run = app.Execute(new[] { "add", "project", refProjFile });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
// csproj contents
using (var csprojStream = new FileInfo(Path.Join(_tempDir.Root, "testproj.csproj")).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.Contains($"<OpenApiProjectReference Include=\"{refProjFile}\"", content);
}
var remove = GetApplication();
run = app.Execute(new[] { "remove", refProjFile });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
// csproj contents
using (var csprojStream = new FileInfo(Path.Join(_tempDir.Root, "testproj.csproj")).OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.DoesNotContain($"<OpenApiProjectReference Include=\"{refProjFile}\"", content);
}
}
[Fact]
public async Task OpenApi_Remove_Multiple()
{
var nswagJsonFile = "openapi.json";
var swagFile2 = "swag2.json";
_tempDir
.WithCSharpProject("testproj")
.WithTargetFrameworks("netcoreapp3.0")
.Dir()
.WithContentFile(nswagJsonFile)
.WithFile(swagFile2)
.WithContentFile("Startup.cs")
.Create();
var add = GetApplication();
var run = add.Execute(new[] { "add", "file", nswagJsonFile });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
add = GetApplication();
run = add.Execute(new[] { "add", "file", swagFile2 });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, run);
var remove = GetApplication();
var removeRun = remove.Execute(new[] { "remove", nswagJsonFile, swagFile2 });
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
Assert.Equal(0, removeRun);
// csproj contents
var csproj = new FileInfo(Path.Join(_tempDir.Root, "testproj.csproj"));
using (var csprojStream = csproj.OpenRead())
using (var reader = new StreamReader(csprojStream))
{
var content = await reader.ReadToEndAsync();
// Don't remove the package reference, they might have taken other dependencies on it
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
Assert.DoesNotContain($"<OpenApiReference Include=\"{nswagJsonFile}\"", content);
}
Assert.False(File.Exists(Path.Combine(_tempDir.Root, nswagJsonFile)));
}
}
}

View File

@ -0,0 +1,183 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Microsoft.DotNet.Openapi.Tools;
using Microsoft.Extensions.Tools.Internal;
using Xunit.Abstractions;
namespace Microsoft.DotNet.OpenApi.Tests
{
public class OpenApiTestBase : IDisposable
{
protected readonly TemporaryDirectory _tempDir;
protected readonly TextWriter _output = new StringWriter();
protected readonly TextWriter _error = new StringWriter();
protected readonly ITestOutputHelper _outputHelper;
protected const string Content = @"{""x-generator"": ""NSwag""}";
protected const string ActualUrl = "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/api-with-examples.yaml";
protected const string BrokenUrl = "https://www.microsoft.com/en-us/dingos.json";
protected const string FakeOpenApiUrl = "https://contoso.com/openapi.json";
protected const string NoDispositionUrl = "https://contoso.com/nodisposition.yaml";
protected const string NoExtensionUrl = "https://contoso.com/noextension";
protected const string NoSegmentUrl = "https://contoso.com";
protected const string DifferentUrl = "https://contoso.com/different.json";
protected const string PackageUrl = "https://go.microsoft.com/fwlink/?linkid=2099561";
protected const string DifferentUrlContent = @"
{
""x-generator"": ""NSwag""
}";
protected const string PackageUrlContent = @"
{
""Version"" : ""1.0"",
""Packages"" : {
""Microsoft.Azure.SignalR"": ""1.1.0-preview1-10442"",
""Grpc.AspNetCore.Server"": ""0.1.22-pre2"",
""Grpc.Net.ClientFactory"": ""0.1.22-pre2"",
""Google.Protobuf"": ""3.8.0"",
""Grpc.Tools"": ""1.22.0"",
""NSwag.ApiDescription.Client"": ""13.0.3"",
""Microsoft.Extensions.ApiDescription.Client"": ""0.3.0-preview7.19365.7"",
""Newtonsoft.Json"": ""12.0.2""
}
}";
public OpenApiTestBase(ITestOutputHelper output)
{
_tempDir = new TemporaryDirectory();
_outputHelper = output;
}
public TemporaryNSwagProject CreateBasicProject(bool withOpenApi)
{
var nswagJsonFile = "openapi.json";
var project = _tempDir
.WithCSharpProject("testproj", sdk: "Microsoft.NET.Sdk.Web")
.WithTargetFrameworks("netcoreapp3.0");
var tmp = project.Dir();
if (withOpenApi)
{
tmp = tmp.WithContentFile(nswagJsonFile);
}
tmp.WithContentFile("Startup.cs")
.Create();
return new TemporaryNSwagProject(project, nswagJsonFile);
}
internal Application GetApplication(bool realHttp = false)
{
IHttpClientWrapper wrapper;
if (realHttp)
{
wrapper = new HttpClientWrapper(new HttpClient());
}
else
{
wrapper = new TestHttpClientWrapper(DownloadMock());
}
return new Application(
_tempDir.Root, wrapper, _output, _error);
}
private IDictionary<string, Tuple<string, ContentDispositionHeaderValue>> DownloadMock()
{
var noExtension = new ContentDispositionHeaderValue("attachment");
noExtension.Parameters.Add(new NameValueHeaderValue("filename", "filename"));
var extension = new ContentDispositionHeaderValue("attachment");
extension.Parameters.Add(new NameValueHeaderValue("filename", "filename.json"));
var justAttachments = new ContentDispositionHeaderValue("attachment");
return new Dictionary<string, Tuple<string, ContentDispositionHeaderValue>> {
{ FakeOpenApiUrl, Tuple.Create(Content, extension)},
{ DifferentUrl, Tuple.Create<string, ContentDispositionHeaderValue>(DifferentUrlContent, null) },
{ PackageUrl, Tuple.Create<string, ContentDispositionHeaderValue>(PackageUrlContent, null) },
{ NoDispositionUrl, Tuple.Create<string, ContentDispositionHeaderValue>(Content, null) },
{ NoExtensionUrl, Tuple.Create(Content, noExtension) },
{ NoSegmentUrl, Tuple.Create(Content, justAttachments) }
};
}
public void Dispose()
{
_outputHelper.WriteLine(_output.ToString());
_tempDir.Dispose();
}
}
public class TestHttpClientWrapper : IHttpClientWrapper
{
private readonly IDictionary<string, Tuple<string, ContentDispositionHeaderValue>> _results;
public TestHttpClientWrapper(IDictionary<string, Tuple<string, ContentDispositionHeaderValue>> results)
{
_results = results;
}
public void Dispose()
{
}
public Task<IHttpResponseMessageWrapper> GetResponseAsync(string url)
{
var result = _results[url];
byte[] byteArray = Encoding.ASCII.GetBytes(result.Item1);
var stream = new MemoryStream(byteArray);
return Task.FromResult<IHttpResponseMessageWrapper>(new TestHttpResponseMessageWrapper(stream, result.Item2));
}
}
public class TestHttpResponseMessageWrapper : IHttpResponseMessageWrapper
{
public Task<Stream> Stream { get; }
public HttpStatusCode StatusCode { get; } = HttpStatusCode.OK;
public bool IsSuccessCode()
{
return true;
}
private ContentDispositionHeaderValue _contentDisposition;
public TestHttpResponseMessageWrapper(
MemoryStream stream,
ContentDispositionHeaderValue header)
{
Stream = Task.FromResult<Stream>(stream);
_contentDisposition = header;
}
public ContentDispositionHeaderValue ContentDisposition()
{
return _contentDisposition;
}
public void Dispose()
{
}
}
public class TemporaryNSwagProject
{
public TemporaryNSwagProject(TemporaryCSharpProject project, string jsonFile)
{
Project = project;
NSwagJsonFile = jsonFile;
}
public TemporaryCSharpProject Project { get; set; }
public string NSwagJsonFile { get; set; }
}
}

View File

@ -0,0 +1,146 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Xunit.Abstractions;
namespace Microsoft.Extensions.Internal
{
internal class ProcessEx : IDisposable
{
private readonly ITestOutputHelper _output;
private readonly Process _process;
private readonly StringBuilder _stderrCapture = new StringBuilder();
private readonly StringBuilder _stdoutCapture = new StringBuilder();
private readonly object _pipeCaptureLock = new object();
private BlockingCollection<string> _stdoutLines = new BlockingCollection<string>();
private TaskCompletionSource<int> _exited;
private ProcessEx(ITestOutputHelper output, Process proc)
{
_output = output;
_process = proc;
proc.EnableRaisingEvents = true;
proc.OutputDataReceived += OnOutputData;
proc.ErrorDataReceived += OnErrorData;
proc.Exited += OnProcessExited;
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
_exited = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
}
public Task Exited => _exited.Task;
public bool HasExited => _process.HasExited;
public string Output
{
get
{
lock (_pipeCaptureLock)
{
return _stdoutCapture.ToString();
}
}
}
public int ExitCode => _process.ExitCode;
public static ProcessEx Run(ITestOutputHelper output, string workingDirectory, string command, string args = null, IDictionary<string, string> envVars = null)
{
var startInfo = new ProcessStartInfo(command, args)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = workingDirectory
};
if (envVars != null)
{
foreach (var envVar in envVars)
{
startInfo.EnvironmentVariables[envVar.Key] = envVar.Value;
}
}
output.WriteLine($"==> {startInfo.FileName} {startInfo.Arguments} [{startInfo.WorkingDirectory}]");
var proc = Process.Start(startInfo);
return new ProcessEx(output, proc);
}
private void OnErrorData(object sender, DataReceivedEventArgs e)
{
if (e.Data == null)
{
return;
}
lock (_pipeCaptureLock)
{
_stderrCapture.AppendLine(e.Data);
}
_output.WriteLine("[ERROR] " + e.Data);
}
private void OnOutputData(object sender, DataReceivedEventArgs e)
{
if (e.Data == null)
{
return;
}
lock (_pipeCaptureLock)
{
_stdoutCapture.AppendLine(e.Data);
}
_output.WriteLine(e.Data);
if (_stdoutLines != null)
{
_stdoutLines.Add(e.Data);
}
}
private void OnProcessExited(object sender, EventArgs e)
{
_process.WaitForExit();
_stdoutLines.CompleteAdding();
_stdoutLines = null;
_exited.TrySetResult(_process.ExitCode);
}
public void Dispose()
{
if (_process != null && !_process.HasExited)
{
_process.KillTree();
}
_process.CancelOutputRead();
_process.CancelErrorRead();
_process.ErrorDataReceived -= OnErrorData;
_process.OutputDataReceived -= OnOutputData;
_process.Exited -= OnProcessExited;
_process.Dispose();
if(_stdoutLines != null)
{
_stdoutLines.Dispose();
}
}
}
}

View File

@ -0,0 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit;
[assembly: CollectionBehavior(DisableTestParallelization = true)]

View File

@ -0,0 +1,46 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Net.Http.Headers;
namespace SimpleWebSite
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Example 1
services
.AddMvcCore()
.AddAuthorization()
.AddFormatterMappings(m => m.SetMediaTypeMappingForFormat("js", new MediaTypeHeaderValue("application/json")))
.SetCompatibilityVersion(CompatibilityVersion.Latest);
}
public void Configure(IApplicationBuilder app)
{
app.UseMvcWithDefaultRoute();
}
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args)
.Build();
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
new WebHostBuilder()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.UseKestrel()
.UseIISIntegration();
}
}

View File

@ -0,0 +1,514 @@
{
"x-generator": "NSwag v11.17.15.0 (NJsonSchema v9.10.53.0 (Newtonsoft.Json v10.0.0.0))",
"openapi": "2.0",
"info": {
"title": "My Title",
"version": "1.0.0"
},
"host": "localhost:44370",
"schemes": [
"https"
],
"consumes": [
"application/json",
"application/json-patch+json",
"text/json",
"application/*+json",
"multipart/form-data"
],
"produces": [
"application/json"
],
"paths": {
"/pet": {
"post": {
"tags": [
"Pet"
],
"operationId": "Pet_AddPet",
"consumes": [
"application/json"
],
"parameters": [
{
"name": "pet",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Pet"
},
"x-nullable": true
}
],
"responses": {
"204": {
"description": ""
},
"400": {
"x-nullable": true,
"description": "",
"schema": {
"$ref": "#/definitions/SerializableError"
}
}
}
},
"put": {
"tags": [
"Pet"
],
"operationId": "Pet_EditPet",
"consumes": [
"application/json"
],
"parameters": [
{
"name": "pet",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Pet"
},
"x-nullable": true
}
],
"responses": {
"204": {
"description": ""
},
"400": {
"x-nullable": true,
"description": "",
"schema": {
"$ref": "#/definitions/SerializableError"
}
}
}
}
},
"/pet/findByStatus": {
"get": {
"tags": [
"Pet"
],
"operationId": "Pet_FindByStatus",
"consumes": [
"application/json-patch+json",
"application/json",
"text/json",
"application/*+json"
],
"parameters": [
{
"name": "status",
"in": "body",
"required": true,
"schema": {
"type": "array",
"items": {
"type": "string"
}
},
"x-nullable": true
}
],
"responses": {
"200": {
"x-nullable": true,
"description": "",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Pet"
}
}
}
}
}
},
"/pet/findByCategory": {
"get": {
"tags": [
"Pet"
],
"operationId": "Pet_FindByCategory",
"parameters": [
{
"type": "string",
"name": "category",
"in": "query",
"required": true,
"x-nullable": true
}
],
"responses": {
"200": {
"x-nullable": true,
"description": "",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Pet"
}
}
},
"400": {
"x-nullable": true,
"description": "",
"schema": {
"$ref": "#/definitions/SerializableError"
}
}
}
}
},
"/pet/{petId}": {
"get": {
"tags": [
"Pet"
],
"operationId": "Pet_FindById",
"parameters": [
{
"type": "integer",
"name": "petId",
"in": "path",
"required": true,
"format": "int32",
"x-nullable": false
}
],
"responses": {
"200": {
"x-nullable": true,
"description": "",
"schema": {
"$ref": "#/definitions/Pet"
}
},
"400": {
"x-nullable": true,
"description": "",
"schema": {
"$ref": "#/definitions/SerializableError"
}
},
"404": {
"description": ""
}
}
},
"post": {
"tags": [
"Pet"
],
"operationId": "Pet_EditPet2",
"parameters": [
{
"type": "integer",
"name": "petId",
"in": "path",
"required": true,
"format": "int32",
"x-nullable": false
},
{
"type": "integer",
"name": "Id",
"in": "formData",
"required": true,
"format": "int32",
"x-nullable": false
},
{
"type": "integer",
"name": "Age",
"in": "formData",
"required": true,
"format": "int32",
"x-nullable": false
},
{
"type": "integer",
"name": "Category.Id",
"in": "formData",
"required": true,
"format": "int32",
"x-nullable": false
},
{
"type": "string",
"name": "Category.Name",
"in": "formData",
"required": true,
"x-nullable": true
},
{
"type": "boolean",
"name": "HasVaccinations",
"in": "formData",
"required": true,
"x-nullable": false
},
{
"type": "string",
"name": "Name",
"in": "formData",
"required": true,
"x-nullable": true
},
{
"type": "array",
"name": "Images",
"in": "formData",
"required": true,
"collectionFormat": "multi",
"x-nullable": true,
"items": {
"$ref": "#/definitions/Image"
}
},
{
"type": "array",
"name": "Tags",
"in": "formData",
"required": true,
"collectionFormat": "multi",
"x-nullable": true,
"items": {
"$ref": "#/definitions/Tag"
}
},
{
"type": "string",
"name": "Status",
"in": "formData",
"required": true,
"x-nullable": true
}
],
"responses": {
"204": {
"description": ""
},
"400": {
"x-nullable": true,
"description": "",
"schema": {
"$ref": "#/definitions/SerializableError"
}
},
"404": {
"description": ""
}
}
},
"delete": {
"tags": [
"Pet"
],
"operationId": "Pet_DeletePet",
"parameters": [
{
"type": "integer",
"name": "petId",
"in": "path",
"required": true,
"format": "int32",
"x-nullable": false
}
],
"responses": {
"204": {
"description": ""
},
"400": {
"x-nullable": true,
"description": "",
"schema": {
"$ref": "#/definitions/SerializableError"
}
},
"404": {
"description": ""
}
}
}
},
"/pet/{petId}/uploadImage": {
"post": {
"tags": [
"Pet"
],
"operationId": "Pet_UploadImage",
"consumes": [
"multipart/form-data"
],
"parameters": [
{
"type": "integer",
"name": "petId",
"in": "path",
"required": true,
"format": "int32",
"x-nullable": false
},
{
"type": "file",
"name": "file",
"in": "formData",
"required": true,
"x-nullable": true
}
],
"responses": {
"200": {
"x-nullable": true,
"description": "",
"schema": {
"$ref": "#/definitions/ApiResponse"
}
},
"400": {
"x-nullable": true,
"description": "",
"schema": {
"$ref": "#/definitions/SerializableError"
}
},
"404": {
"description": ""
}
}
}
}
},
"definitions": {
"Pet": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"age",
"hasVaccinations",
"name",
"status"
],
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"age": {
"type": "integer",
"format": "int32",
"maximum": 150.0,
"minimum": 0.0
},
"category": {
"$ref": "#/definitions/Category"
},
"hasVaccinations": {
"type": "boolean"
},
"name": {
"type": "string",
"maxLength": 50,
"minLength": 2
},
"images": {
"type": "array",
"items": {
"$ref": "#/definitions/Image"
}
},
"tags": {
"type": "array",
"items": {
"$ref": "#/definitions/Tag"
}
},
"status": {
"type": "string"
}
}
},
"Category": {
"type": "object",
"additionalProperties": false,
"required": [
"id"
],
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"name": {
"type": "string"
}
}
},
"Image": {
"type": "object",
"additionalProperties": false,
"required": [
"id"
],
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"url": {
"type": "string"
}
}
},
"Tag": {
"type": "object",
"additionalProperties": false,
"required": [
"id"
],
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"name": {
"type": "string"
}
}
},
"SerializableError": {
"type": "object",
"additionalProperties": false,
"allOf": [
{
"type": "object",
"additionalProperties": {}
}
]
},
"ApiResponse": {
"type": "object",
"additionalProperties": false,
"required": [
"code"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"type": {
"type": "string"
}
}
}
}
}

View File

@ -0,0 +1,51 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<AssemblyName>Microsoft.DotNet.Open.Api.Tools.Tests</AssemblyName>
<DefaultItemExcludes>$(DefaultItemExcludes);TestProjects\**\*</DefaultItemExcludes>
<TestGroupName>DotNetAddOpenAPIReferenceToolsTests</TestGroupName>
</PropertyGroup>
<PropertyGroup>
<OpenAPIToolCSProjPath>..\src\Microsoft.dotnet-openapi.csproj</OpenAPIToolCSProjPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(ToolSharedSourceRoot)TestHelpers\**\*.cs" />
<Compile Include="$(SharedSourceRoot)test\SkipOnHelixAttribute.cs" />
<Content Include="TestContent\*" LinkBase="TestContent\" CopyToOutputDirectory="PreserveNewest" />
<Compile Include="$(SharedSourceRoot)Process\ProcessExtensions.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Build" ExcludeAssets="runtime" />
<ProjectReference Include="$(OpenAPIToolCSProjPath)" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>TestSettings:RestoreSources</_Parameter1>
<_Parameter2>$(RestoreSources)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>TestSettings:RuntimeFrameworkVersion</_Parameter1>
<_Parameter2>$(RuntimeFrameworkVersion)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>RepoRoot</_Parameter1>
<_Parameter2>$(RepoRoot)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
<Target Name="CleanTestProjects" BeforeTargets="CoreCompile">
<RemoveDir Directories="$(TargetDir)TestProjects" Condition="Exists('$(TargetDir)TestProjects')" />
</Target>
<Target Name="PublishDotNetOpenApiOnBuild" BeforeTargets="Build" Condition="'$(DotNetBuildFromSource)' != 'true'">
<MSBuild Projects="$(OpenAPIToolCSProjPath)" Targets="Publish" Properties="PublishDir=$(OutputPath)\tool\;Configuration=$(Configuration)" />
</Target>
<Target Name="PublishDotNetOpenApiOnPublish" BeforeTargets="Publish" Condition="'$(DotNetBuildFromSource)' != 'true'">
<MSBuild Projects="$(OpenAPIToolCSProjPath)" Targets="Publish" Properties="PublishDir=$(PublishDir)\tool\;Configuration=$(Configuration)" />
</Target>
</Project>

View File

@ -0,0 +1,5 @@
{
"longRunningTestSeconds": 30,
"diagnosticMessages": true,
"maxParallelThreads": -1
}

View File

@ -1,18 +1,24 @@
# DotNetTools
## Projects
## Bundled tools
The folder contains command-line tools for ASP.NET Core that are bundled* in the .NET Core CLI. Follow the links below for more details on each tool.
The folder contains command-line tools for ASP.NET Core. The following tools are bundled* in the .NET Core CLI. Follow the links below for more details on each tool.
- [dotnet-watch](dotnet-watch/README.md)
- [dotnet-user-secrets](dotnet-user-secrets/README.md)
- [dotnet-sql-cache](dotnet-sql-cache/README.md)
- [dotnet-dev-certs](dotnet-dev-certs/README.md)
- [dotnet-watch](dotnet-watch/README.md)
- [dotnet-user-secrets](dotnet-user-secrets/README.md)
- [dotnet-sql-cache](dotnet-sql-cache/README.md)
- [dotnet-dev-certs](dotnet-dev-certs/README.md)
*\*This applies to .NET Core CLI 2.1.300-preview2 and up. For earlier versions of the CLI, these tools must be installed separately.*
*For 2.0 CLI and earlier, see <https://github.com/aspnet/DotNetTools/tree/rel/2.0.0/README.md> for details.*
## Non-bundled tools
The following tools are produced by us but not bundled in the .NET Core CLI. They must be aquired independently.
- [Microsoft.dotnet-openapi](Microsoft.dotnet-openapi/README.md)
This folder also contains the infrastructure for our partners' service reference features:
- [Extensions.ApiDescription.Client](Extensions.ApiDescription.Client/README.md) MSBuild glue for OpenAPI code generation.
@ -29,10 +35,11 @@ dotnet watch
dotnet user-secrets
dotnet sql-cache
dotnet dev-certs
dotnet openapi
```
Add `--help` to see more details. For example,
```
```sh
dotnet watch --help
```

View File

@ -3,6 +3,7 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
namespace Microsoft.Extensions.CommandLineUtils
{

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -6,12 +6,12 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
namespace Microsoft.DotNet.Watcher.Tools.Tests
namespace Microsoft.Extensions.Tools.Internal
{
public class TemporaryCSharpProject
{
private const string Template =
@"<Project Sdk=""Microsoft.NET.Sdk"">
@"<Project Sdk=""{2}"">
<PropertyGroup>
{0}
<OutputType>Exe</OutputType>
@ -23,19 +23,22 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
private readonly string _filename;
private readonly TemporaryDirectory _directory;
private List<string> _items = new List<string>();
private List<string> _properties = new List<string>();
private readonly List<string> _items = new List<string>();
private readonly List<string> _properties = new List<string>();
public TemporaryCSharpProject(string name, TemporaryDirectory directory)
public TemporaryCSharpProject(string name, TemporaryDirectory directory, string sdk)
{
Name = name;
_filename = name + ".csproj";
_directory = directory;
Sdk = sdk;
}
public string Name { get; }
public string Path => System.IO.Path.Combine(_directory.Root, _filename);
public string Sdk { get; }
public TemporaryCSharpProject WithTargetFrameworks(params string[] tfms)
{
Debug.Assert(tfms.Length > 0);
@ -95,7 +98,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
public void Create()
{
_directory.CreateFile(_filename, string.Format(Template, string.Join("\r\n", _properties), string.Join("\r\n", _items)));
_directory.CreateFile(_filename, string.Format(Template, string.Join("\r\n", _properties), string.Join("\r\n", _items), Sdk));
}
public class ItemSpec

View File

@ -5,7 +5,7 @@ using System;
using System.Collections.Generic;
using System.IO;
namespace Microsoft.DotNet.Watcher.Tools.Tests
namespace Microsoft.Extensions.Tools.Internal
{
public class TemporaryDirectory : IDisposable
{
@ -16,7 +16,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
public TemporaryDirectory()
{
Root = Path.Combine(Path.GetTempPath(), "dotnet-watch-tests", Guid.NewGuid().ToString("N"));
Root = Path.Combine(Path.GetTempPath(), "dotnet-tool-tests", Guid.NewGuid().ToString("N"));
}
private TemporaryDirectory(string path, TemporaryDirectory parent)
@ -34,16 +34,16 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
public string Root { get; }
public TemporaryCSharpProject WithCSharpProject(string name)
public TemporaryCSharpProject WithCSharpProject(string name, string sdk = "Microsoft.NET.Sdk")
{
var project = new TemporaryCSharpProject(name, this);
var project = new TemporaryCSharpProject(name, this, sdk);
_projects.Add(project);
return project;
}
public TemporaryCSharpProject WithCSharpProject(string name, out TemporaryCSharpProject project)
public TemporaryCSharpProject WithCSharpProject(string name, out TemporaryCSharpProject project, string sdk = "Microsoft.NET.Sdk")
{
project = WithCSharpProject(name);
project = WithCSharpProject(name, sdk);
return project;
}
@ -53,6 +53,16 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
return this;
}
public TemporaryDirectory WithContentFile(string name)
{
using (var stream = File.OpenRead(Path.Combine("TestContent", $"{name}.txt")))
using (var streamReader = new StreamReader(stream))
{
_files[name] = streamReader.ReadToEnd();
}
return this;
}
public TemporaryDirectory Up()
{
if (_parent == null)

View File

@ -7,13 +7,21 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-watch", "dotnet-watc
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-watch.Tests", "dotnet-watch\test\dotnet-watch.Tests.csproj", "{63F7E822-D1E2-4C41-8ABF-60B9E3A9C54C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-dev-certs", "dotnet-dev-certs\src\dotnet-dev-certs.csproj", "{0D6D5693-7E0C-4FE8-B4AA-21207B2650AA}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E01EE27B-6CF9-4707-9849-5BA2ABA825F2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DeveloperCertificates.XPlat", "FirstRunCertGenerator\src\Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj", "{7BBDBDA2-299F-4C36-8338-23C525901DE0}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{2C485EAF-E4DE-4D14-8AE1-330641E17D44}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DeveloperCertificates.XPlat.Tests", "FirstRunCertGenerator\test\Microsoft.AspNetCore.DeveloperCertificates.XPlat.Tests.csproj", "{1EC6FA27-40A5-433F-8CA1-636E7ED8863E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-dev-certs", "dotnet-dev-certs\src\dotnet-dev-certs.csproj", "{98550159-E04E-44EB-A969-E5BF12444B94}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-sql-cache", "dotnet-sql-cache\src\dotnet-sql-cache.csproj", "{15FB0E39-1A28-4325-AD3C-76352516C80D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-sql-cache", "dotnet-sql-cache\src\dotnet-sql-cache.csproj", "{216AF7F1-5B05-477E-B8D3-86F6059F268A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-secrets", "dotnet-user-secrets\src\dotnet-user-secrets.csproj", "{5FE62357-2915-4890-813A-D82656BDC4DD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-secrets.Tests", "dotnet-user-secrets\test\dotnet-user-secrets.Tests.csproj", "{25F8DCC4-4571-42F7-BA0F-5C2D5A802297}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.dotnet-openapi", "Microsoft.dotnet-openapi\src\Microsoft.dotnet-openapi.csproj", "{C806041C-30F2-4B27-918A-5FF3576B833B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-microsoft.openapi.Tests", "Microsoft.dotnet-openapi\test\dotnet-microsoft.openapi.Tests.csproj", "{26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions.ApiDescription.Client", "Extensions.ApiDescription.Client", "{78610083-1FCE-47F5-AB4D-AF0E1313B351}"
EndProject
@ -77,6 +85,30 @@ Global
{EB63AECB-B898-475D-90F7-FE61F9C1CCC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB63AECB-B898-475D-90F7-FE61F9C1CCC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB63AECB-B898-475D-90F7-FE61F9C1CCC6}.Release|Any CPU.Build.0 = Release|Any CPU
{98550159-E04E-44EB-A969-E5BF12444B94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{98550159-E04E-44EB-A969-E5BF12444B94}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98550159-E04E-44EB-A969-E5BF12444B94}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98550159-E04E-44EB-A969-E5BF12444B94}.Release|Any CPU.Build.0 = Release|Any CPU
{216AF7F1-5B05-477E-B8D3-86F6059F268A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{216AF7F1-5B05-477E-B8D3-86F6059F268A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{216AF7F1-5B05-477E-B8D3-86F6059F268A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{216AF7F1-5B05-477E-B8D3-86F6059F268A}.Release|Any CPU.Build.0 = Release|Any CPU
{5FE62357-2915-4890-813A-D82656BDC4DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5FE62357-2915-4890-813A-D82656BDC4DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5FE62357-2915-4890-813A-D82656BDC4DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5FE62357-2915-4890-813A-D82656BDC4DD}.Release|Any CPU.Build.0 = Release|Any CPU
{25F8DCC4-4571-42F7-BA0F-5C2D5A802297}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25F8DCC4-4571-42F7-BA0F-5C2D5A802297}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25F8DCC4-4571-42F7-BA0F-5C2D5A802297}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25F8DCC4-4571-42F7-BA0F-5C2D5A802297}.Release|Any CPU.Build.0 = Release|Any CPU
{C806041C-30F2-4B27-918A-5FF3576B833B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C806041C-30F2-4B27-918A-5FF3576B833B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C806041C-30F2-4B27-918A-5FF3576B833B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C806041C-30F2-4B27-918A-5FF3576B833B}.Release|Any CPU.Build.0 = Release|Any CPU
{26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -88,6 +120,14 @@ Global
{160A445F-7E1F-430D-9403-41F7F6F4A16E} = {4110117E-3C28-4064-A7A3-B112BD6F8CB9}
{233119FC-E4C1-421C-89AE-1A445C5A947F} = {4110117E-3C28-4064-A7A3-B112BD6F8CB9}
{EB63AECB-B898-475D-90F7-FE61F9C1CCC6} = {4110117E-3C28-4064-A7A3-B112BD6F8CB9}
{E16F10C8-5FC3-420B-AB33-D6E5CBE89B75} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2}
{63F7E822-D1E2-4C41-8ABF-60B9E3A9C54C} = {2C485EAF-E4DE-4D14-8AE1-330641E17D44}
{98550159-E04E-44EB-A969-E5BF12444B94} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2}
{216AF7F1-5B05-477E-B8D3-86F6059F268A} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2}
{5FE62357-2915-4890-813A-D82656BDC4DD} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2}
{25F8DCC4-4571-42F7-BA0F-5C2D5A802297} = {2C485EAF-E4DE-4D14-8AE1-330641E17D44}
{C806041C-30F2-4B27-918A-5FF3576B833B} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2}
{26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F} = {2C485EAF-E4DE-4D14-8AE1-330641E17D44}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {EC668D8E-97B9-4758-9E5C-2E5DD6B9137B}

3
src/Tools/build.cmd Normal file
View File

@ -0,0 +1,3 @@
@ECHO OFF
SET RepoRoot=%~dp0..\..
%RepoRoot%\build.cmd -projects %~dp0\**\*.*proj %*

7
src/Tools/build.sh Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
repo_root="$DIR/../.."
"$repo_root/build.sh" --projects "$DIR/**/*.*proj" "$@"

View File

@ -104,10 +104,10 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
app.HelpOption("-h|--help");
app.OnExecute(() =>
{
app.ShowHelp();
return Success;
});
{
app.ShowHelp();
return Success;
});
return app.Execute(args);
}

View File

@ -11,18 +11,20 @@ namespace Microsoft.DotNet.Watcher
{
private object _lock = new object();
public PrefixConsoleReporter(IConsole console, bool verbose, bool quiet)
private readonly string _prefix;
public PrefixConsoleReporter(string prefix, IConsole console, bool verbose, bool quiet)
: base(console, verbose, quiet)
{ }
{
_prefix = prefix;
}
protected override void WriteLine(TextWriter writer, string message, ConsoleColor? color)
{
const string prefix = "watch : ";
lock (_lock)
{
Console.ForegroundColor = ConsoleColor.DarkGray;
writer.Write(prefix);
writer.Write(_prefix);
Console.ResetColor();
base.WriteLine(writer, message, color);

View File

@ -15,17 +15,17 @@ namespace Microsoft.DotNet.Watcher
public class Program : IDisposable
{
private readonly IConsole _console;
private readonly string _workingDir;
private readonly string _workingDirectory;
private readonly CancellationTokenSource _cts;
private IReporter _reporter;
public Program(IConsole console, string workingDir)
public Program(IConsole console, string workingDirectory)
{
Ensure.NotNull(console, nameof(console));
Ensure.NotNullOrEmpty(workingDir, nameof(workingDir));
Ensure.NotNullOrEmpty(workingDirectory, nameof(workingDirectory));
_console = console;
_workingDir = workingDir;
_workingDirectory = workingDirectory;
_cts = new CancellationTokenSource();
_console.CancelKeyPress += OnCancelKeyPress;
_reporter = CreateReporter(verbose: true, quiet: false, console: _console);
@ -134,7 +134,7 @@ namespace Microsoft.DotNet.Watcher
string projectFile;
try
{
projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, project);
projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDirectory, project);
}
catch (FileNotFoundException ex)
{
@ -177,7 +177,7 @@ namespace Microsoft.DotNet.Watcher
string projectFile;
try
{
projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, project);
projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDirectory, project);
}
catch (FileNotFoundException ex)
{
@ -205,7 +205,7 @@ namespace Microsoft.DotNet.Watcher
}
private static IReporter CreateReporter(bool verbose, bool quiet, IConsole console)
=> new PrefixConsoleReporter(console, verbose || CliContext.IsGlobalVerbose(), quiet);
=> new PrefixConsoleReporter("watch : ", console, verbose || CliContext.IsGlobalVerbose(), quiet);
public void Dispose()
{

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="$(SharedSourceRoot)Process\*.cs" />
<Compile Include="$(SharedSourceRoot)Process\ProcessExtensions.cs" />
<Compile Include="$(ToolSharedSourceRoot)CommandLine\**\*.cs" />
<None Include="assets\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

View File

@ -1,11 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Xunit;
using Xunit.Sdk;
namespace Microsoft.DotNet.Watcher.Tools.Tests

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -113,17 +113,20 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
public async Task MultiTfm()
{
_tempDir
.SubDir("src")
.SubDir("Project1")
.WithCSharpProject("Project1", out var target)
.WithTargetFrameworks("netcoreapp1.0", "net451")
.WithProperty("EnableDefaultCompileItems", "false")
.WithItem("Compile", "Class1.netcore.cs", "'$(TargetFramework)'=='netcoreapp1.0'")
.WithItem("Compile", "Class1.desktop.cs", "'$(TargetFramework)'=='net451'")
.Dir()
.WithFile("Class1.netcore.cs")
.WithFile("Class1.desktop.cs")
.WithFile("Class1.notincluded.cs");
.SubDir("src")
.SubDir("Project1")
.WithCSharpProject("Project1", out var target)
.WithTargetFrameworks("netcoreapp1.0", "net451")
.WithProperty("EnableDefaultCompileItems", "false")
.WithItem("Compile", "Class1.netcore.cs", "'$(TargetFramework)'=='netcoreapp1.0'")
.WithItem("Compile", "Class1.desktop.cs", "'$(TargetFramework)'=='net451'")
.Dir()
.WithFile("Class1.netcore.cs")
.WithFile("Class1.desktop.cs")
.WithFile("Class1.notincluded.cs")
.Up()
.Up()
.Create();
var fileset = await GetFileSet(target);
@ -155,7 +158,10 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
.WithTargetFrameworks("netcoreapp1.0", "net451")
.WithProjectReference(proj2)
.Dir()
.WithFile("Class1.cs");
.WithFile("Class1.cs")
.Up()
.Up()
.Create();
var fileset = await GetFileSet(target);

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher.Tools.Tests
{
@ -10,7 +11,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
{
private readonly TemporaryDirectory _directory;
private Action<TemporaryCSharpProject> _onCreate;
private Dictionary<string, TemporaryCSharpProject> _projects = new Dictionary<string, TemporaryCSharpProject>();
private readonly Dictionary<string, TemporaryCSharpProject> _projects = new Dictionary<string, TemporaryCSharpProject>();
public TestProjectGraph(TemporaryDirectory directory)
{
_directory = directory;
@ -28,8 +29,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
public TemporaryCSharpProject GetOrCreate(string projectName)
{
TemporaryCSharpProject sourceProj;
if (!_projects.TryGetValue(projectName, out sourceProj))
if (!_projects.TryGetValue(projectName, out TemporaryCSharpProject sourceProj))
{
sourceProj = _directory.SubDir(projectName).WithCSharpProject(projectName);
_onCreate?.Invoke(sourceProj);

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>