diff --git a/.gitmodules b/.gitmodules
index 837bee2191..fa31e929f1 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -30,10 +30,6 @@
path = modules/Diagnostics
url = https://github.com/aspnet/Diagnostics.git
branch = release/2.2
-[submodule "modules/DotNetTools"]
- path = modules/DotNetTools
- url = https://github.com/aspnet/DotNetTools.git
- branch = release/2.2
[submodule "modules/EntityFrameworkCore"]
path = modules/EntityFrameworkCore
url = https://github.com/aspnet/EntityFrameworkCore.git
diff --git a/build/buildorder.props b/build/buildorder.props
index 740c110947..fb7906a5ab 100644
--- a/build/buildorder.props
+++ b/build/buildorder.props
@@ -7,7 +7,6 @@
-
diff --git a/build/repo.props b/build/repo.props
index 486ea83c12..416ea94183 100644
--- a/build/repo.props
+++ b/build/repo.props
@@ -49,12 +49,16 @@
-
+
diff --git a/build/submodules.props b/build/submodules.props
index 1d429fae9c..3b768bb267 100644
--- a/build/submodules.props
+++ b/build/submodules.props
@@ -54,7 +54,6 @@
-
diff --git a/eng/Dependencies.props b/eng/Dependencies.props
index a88732f80c..dfb4ddbee8 100644
--- a/eng/Dependencies.props
+++ b/eng/Dependencies.props
@@ -7,23 +7,29 @@
+
+
+
+
+
+
diff --git a/eng/NuGetPackageVerifier.json b/eng/NuGetPackageVerifier.json
new file mode 100644
index 0000000000..4ce6d1a168
--- /dev/null
+++ b/eng/NuGetPackageVerifier.json
@@ -0,0 +1,102 @@
+{
+ "adx": {
+ "rules": [
+ "AdxVerificationCompositeRule"
+ ],
+ "packages": {
+ "dotnet-watch": {
+ "packageTypes": [
+ "DotnetTool"
+ ]
+ },
+ "dotnet-sql-cache": {
+ "packageTypes": [
+ "DotnetTool"
+ ],
+ "Exclusions": {
+ "NEUTRAL_RESOURCES_LANGUAGE": {
+ "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
+ },
+ "WRONG_PUBLICKEYTOKEN": {
+ "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/runtimes/unix/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg."
+ },
+ "ASSEMBLY_INFORMATIONAL_VERSION_MISMATCH": {
+ "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/runtimes/unix/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg."
+ },
+ "ASSEMBLY_FILE_VERSION_MISMATCH": {
+ "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/runtimes/unix/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg."
+ },
+ "ASSEMBLY_VERSION_MISMATCH": {
+ "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/runtimes/unix/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg."
+ }
+ }
+ },
+ "dotnet-user-secrets": {
+ "packageTypes": [
+ "DotnetTool"
+ ],
+ "Exclusions": {
+ "NEUTRAL_RESOURCES_LANGUAGE": {
+ "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
+ },
+ "SERVICING_ATTRIBUTE": {
+ "tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg."
+ },
+ "WRONG_PUBLICKEYTOKEN": {
+ "tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
+ },
+ "ASSEMBLY_INFORMATIONAL_VERSION_MISMATCH": {
+ "tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
+ },
+ "ASSEMBLY_FILE_VERSION_MISMATCH": {
+ "tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
+ },
+ "ASSEMBLY_VERSION_MISMATCH": {
+ "tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg.",
+ "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
+ }
+ }
+ },
+ "dotnet-dev-certs": {
+ "packageTypes": [
+ "DotnetTool"
+ ]
+ },
+ "Microsoft.AspNetCore.DeveloperCertificates.XPlat": {
+ "Exclusions": {
+ "DOC_MISSING": {
+ "lib/netcoreapp2.1/Microsoft.AspNetCore.DeveloperCertificates.XPlat.dll": "Docs not required to shipoob package"
+ }
+ }
+ }
+ }
+ },
+ "Default": {
+ "rules": [
+ "DefaultCompositeRule"
+ ]
+ }
+}
diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props
index 0415e44d3e..55d8dc6094 100644
--- a/eng/ProjectReferences.props
+++ b/eng/ProjectReferences.props
@@ -13,6 +13,11 @@
+
+
+
+
+
diff --git a/eng/tools/BaselineGenerator/baseline.xml b/eng/tools/BaselineGenerator/baseline.xml
index 759e73ffa7..f6ec31acb2 100644
--- a/eng/tools/BaselineGenerator/baseline.xml
+++ b/eng/tools/BaselineGenerator/baseline.xml
@@ -1,4 +1,8 @@
+
+
+
+
diff --git a/modules/DotNetTools b/modules/DotNetTools
deleted file mode 160000
index d745b8c161..0000000000
--- a/modules/DotNetTools
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit d745b8c161d42665ceb51597888062336fec7764
diff --git a/src/Tools/Directory.Build.props b/src/Tools/Directory.Build.props
new file mode 100644
index 0000000000..6b35802689
--- /dev/null
+++ b/src/Tools/Directory.Build.props
@@ -0,0 +1,9 @@
+
+
+
+
+ $(RepositoryRoot)obj\$(MSBuildProjectName)\
+ $(RepositoryRoot)bin\$(MSBuildProjectName)\
+
+
+
diff --git a/src/Tools/Directory.Build.targets b/src/Tools/Directory.Build.targets
new file mode 100644
index 0000000000..7928d3f6cf
--- /dev/null
+++ b/src/Tools/Directory.Build.targets
@@ -0,0 +1,11 @@
+
+
+
+
+ true
+
+
+
+
+
+
diff --git a/src/Tools/FirstRunCertGenerator/src/CertificateGenerator.cs b/src/Tools/FirstRunCertGenerator/src/CertificateGenerator.cs
new file mode 100644
index 0000000000..d3f58eae35
--- /dev/null
+++ b/src/Tools/FirstRunCertGenerator/src/CertificateGenerator.cs
@@ -0,0 +1,15 @@
+using System;
+using Microsoft.AspNetCore.Certificates.Generation;
+
+namespace Microsoft.AspNetCore.DeveloperCertificates.XPlat
+{
+ public static class CertificateGenerator
+ {
+ public static void GenerateAspNetHttpsCertificate()
+ {
+ var manager = new CertificateManager();
+ var now = DateTimeOffset.Now;
+ manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1));
+ }
+ }
+}
diff --git a/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj b/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj
new file mode 100644
index 0000000000..ce61b3ab62
--- /dev/null
+++ b/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj
@@ -0,0 +1,14 @@
+
+
+
+ netcoreapp2.1
+ Package for the CLI first run experience.
+ $(DefineConstants);XPLAT
+ aspnet;cli
+
+
+
+
+
+
+
diff --git a/src/Tools/README.md b/src/Tools/README.md
new file mode 100644
index 0000000000..e13c6ec6e3
--- /dev/null
+++ b/src/Tools/README.md
@@ -0,0 +1,32 @@
+DotNetTools
+===========
+
+## Projects
+
+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.
+
+ - [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 for details.*
+
+## Usage
+
+The command line tools can be invoked as a subcommand of `dotnet`.
+
+```sh
+dotnet watch
+dotnet user-secrets
+dotnet sql-cache
+dotnet dev-certs
+```
+
+Add `--help` to see more details. For example,
+
+```
+dotnet watch --help
+```
diff --git a/src/Tools/dotnet-dev-certs/README.md b/src/Tools/dotnet-dev-certs/README.md
new file mode 100644
index 0000000000..e565dbc0e8
--- /dev/null
+++ b/src/Tools/dotnet-dev-certs/README.md
@@ -0,0 +1,8 @@
+dotnet-dev-certs
+================
+
+`dotnet-dev-certs` is a command line tool to generate certificates used in ASP.NET Core during development.
+
+### How To Use
+
+Run `dotnet dev-certs --help` for more information about usage.
diff --git a/src/Tools/dotnet-dev-certs/src/Program.cs b/src/Tools/dotnet-dev-certs/src/Program.cs
new file mode 100644
index 0000000000..170e11b09d
--- /dev/null
+++ b/src/Tools/dotnet-dev-certs/src/Program.cs
@@ -0,0 +1,247 @@
+// Copyright (c) .NET Foundation. 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.Runtime.InteropServices;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Certificates.Generation;
+using Microsoft.Extensions.CommandLineUtils;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
+{
+ internal class Program
+ {
+ private const int CriticalError = -1;
+ private const int Success = 0;
+ private const int ErrorCreatingTheCertificate = 1;
+ private const int ErrorSavingTheCertificate = 2;
+ private const int ErrorExportingTheCertificate = 3;
+ private const int ErrorTrustingTheCertificate = 4;
+ private const int ErrorUserCancelledTrustPrompt = 5;
+ private const int ErrorNoValidCertificateFound = 6;
+ private const int ErrorCertificateNotTrusted = 7;
+ private const int ErrorCleaningUpCertificates = 8;
+
+ public static readonly TimeSpan HttpsCertificateValidity = TimeSpan.FromDays(365);
+
+ public static int Main(string[] args)
+ {
+ try
+ {
+ var app = new CommandLineApplication
+ {
+ Name = "dotnet dev-certs"
+ };
+
+ app.Command("https", c =>
+ {
+ var exportPath = c.Option("-ep|--export-path",
+ "Full path to the exported certificate",
+ CommandOptionType.SingleValue);
+
+ var password = c.Option("-p|--password",
+ "Password to use when exporting the certificate with the private key into a pfx file",
+ CommandOptionType.SingleValue);
+
+ var check = c.Option(
+ "-c|--check",
+ "Check for the existence of the certificate but do not perform any action",
+ CommandOptionType.NoValue);
+
+ var clean = c.Option(
+ "--clean",
+ "Cleans all HTTPS development certificates from the machine.",
+ CommandOptionType.NoValue);
+
+ CommandOption trust = null;
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ trust = c.Option("-t|--trust",
+ "Trust the certificate on the current platform",
+ CommandOptionType.NoValue);
+ }
+
+ var verbose = c.Option("-v|--verbose",
+ "Display more debug information.",
+ CommandOptionType.NoValue);
+
+ var quiet = c.Option("-q|--quiet",
+ "Display warnings and errors only.",
+ CommandOptionType.NoValue);
+
+ c.HelpOption("-h|--help");
+
+ c.OnExecute(() =>
+ {
+ var reporter = new ConsoleReporter(PhysicalConsole.Singleton, verbose.HasValue(), quiet.HasValue());
+ if ((clean.HasValue() && (exportPath.HasValue() || password.HasValue() || trust?.HasValue() == true)) ||
+ (check.HasValue() && (exportPath.HasValue() || password.HasValue() || clean.HasValue())))
+ {
+ reporter.Error(@"Incompatible set of flags. Sample usages
+'dotnet dev-certs https --clean'
+'dotnet dev-certs https --check --trust'
+'dotnet dev-certs https -ep ./certificate.pfx -p password --trust'");
+ }
+
+ if (check.HasValue())
+ {
+ return CheckHttpsCertificate(trust, reporter);
+ }
+
+ if (clean.HasValue())
+ {
+ return CleanHttpsCertificates(reporter);
+ }
+
+ return EnsureHttpsCertificate(exportPath, password, trust, reporter);
+ });
+ });
+
+ app.HelpOption("-h|--help");
+
+ app.OnExecute(() =>
+ {
+ app.ShowHelp();
+ return Success;
+ });
+
+ return app.Execute(args);
+ }
+ catch
+ {
+ return CriticalError;
+ }
+ }
+
+ private static int CleanHttpsCertificates(IReporter reporter)
+ {
+ var manager = new CertificateManager();
+ try
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ reporter.Output("Cleaning HTTPS development certificates from the machine. A prompt might get " +
+ "displayed to confirm the removal of some of the certificates.");
+ }
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ reporter.Output("Cleaning HTTPS development certificates from the machine. This operation might " +
+ "require elevated privileges. If that is the case, a prompt for credentials will be displayed.");
+ }
+
+ manager.CleanupHttpsCertificates();
+ reporter.Verbose("HTTPS development certificates successfully removed from the machine.");
+ return Success;
+ }
+ catch(Exception e)
+ {
+ reporter.Error("There was an error trying to clean HTTPS development certificates on this machine.");
+ reporter.Error(e.Message);
+
+ return ErrorCleaningUpCertificates;
+ }
+ }
+
+ private static int CheckHttpsCertificate(CommandOption trust, IReporter reporter)
+ {
+ var now = DateTimeOffset.Now;
+ var certificateManager = new CertificateManager();
+ var certificates = certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
+ if (certificates.Count == 0)
+ {
+ reporter.Verbose("No valid certificate found.");
+ return ErrorNoValidCertificateFound;
+ }
+ else
+ {
+ reporter.Verbose("A valid certificate was found.");
+ }
+
+ if (trust != null && trust.HasValue())
+ {
+ var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root;
+ var trustedCertificates = certificateManager.ListCertificates(CertificatePurpose.HTTPS, store, StoreLocation.CurrentUser, isValid: true);
+ if (!certificates.Any(c => certificateManager.IsTrusted(c)))
+ {
+ reporter.Verbose($@"The following certificates were found, but none of them is trusted:
+{string.Join(Environment.NewLine, certificates.Select(c => $"{c.Subject} - {c.Thumbprint}"))}");
+ return ErrorCertificateNotTrusted;
+ }
+ else
+ {
+ reporter.Verbose("A trusted certificate was found.");
+ }
+ }
+
+ return Success;
+ }
+
+ private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption trust, IReporter reporter)
+ {
+ var now = DateTimeOffset.Now;
+ var manager = new CertificateManager();
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && trust?.HasValue() == true)
+ {
+ reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " +
+ "already trusted we will run the following command:" + Environment.NewLine +
+ "'sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <>'" +
+ Environment.NewLine + "This command might prompt you for your password to install the certificate " +
+ "on the system keychain.");
+ }
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && trust?.HasValue() == true)
+ {
+ reporter.Warn("Trusting the HTTPS development certificate was requested. A confirmation prompt will be displayed " +
+ "if the certificate was not previously trusted. Click yes on the prompt to trust the certificate.");
+ }
+
+ var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(
+ now,
+ now.Add(HttpsCertificateValidity),
+ exportPath.Value(),
+ trust == null ? false : trust.HasValue(),
+ password.HasValue(),
+ password.Value());
+
+ switch (result)
+ {
+ case EnsureCertificateResult.Succeeded:
+ reporter.Output("The HTTPS developer certificate was generated successfully.");
+ if (exportPath.Value() != null)
+ {
+ reporter.Verbose($"The certificate was exported to {Path.GetFullPath(exportPath.Value())}");
+ }
+ return Success;
+ case EnsureCertificateResult.ValidCertificatePresent:
+ reporter.Output("A valid HTTPS certificate is already present.");
+ if (exportPath.Value() != null)
+ {
+ reporter.Verbose($"The certificate was exported to {Path.GetFullPath(exportPath.Value())}");
+ }
+ return Success;
+ case EnsureCertificateResult.ErrorCreatingTheCertificate:
+ reporter.Error("There was an error creating the HTTPS developer certificate.");
+ return ErrorCreatingTheCertificate;
+ case EnsureCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore:
+ reporter.Error("There was an error saving the HTTPS developer certificate to the current user personal certificate store.");
+ return ErrorSavingTheCertificate;
+ case EnsureCertificateResult.ErrorExportingTheCertificate:
+ reporter.Warn("There was an error exporting HTTPS developer certificate to a file.");
+ return ErrorExportingTheCertificate;
+ case EnsureCertificateResult.FailedToTrustTheCertificate:
+ reporter.Warn("There was an error trusting HTTPS developer certificate.");
+ return ErrorTrustingTheCertificate;
+ case EnsureCertificateResult.UserCancelledTrustStep:
+ reporter.Warn("The user cancelled the trust step.");
+ return ErrorUserCancelledTrustPrompt;
+ default:
+ reporter.Error("Something went wrong. The HTTPS developer certificate could not be created.");
+ return CriticalError;
+ }
+ }
+ }
+}
diff --git a/src/Tools/dotnet-dev-certs/src/dotnet-dev-certs.csproj b/src/Tools/dotnet-dev-certs/src/dotnet-dev-certs.csproj
new file mode 100644
index 0000000000..1f0f906e50
--- /dev/null
+++ b/src/Tools/dotnet-dev-certs/src/dotnet-dev-certs.csproj
@@ -0,0 +1,32 @@
+
+
+
+ netcoreapp2.1
+ exe
+ Command line tool to generate certificates used in ASP.NET Core during development.
+ Microsoft.AspNetCore.DeveloperCertificates.Tools
+ dotnet;developercertificates
+ true
+
+ win-x64;win-x86
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Tools/dotnet-sql-cache/README.md b/src/Tools/dotnet-sql-cache/README.md
new file mode 100644
index 0000000000..141c1cab65
--- /dev/null
+++ b/src/Tools/dotnet-sql-cache/README.md
@@ -0,0 +1,8 @@
+dotnet-sql-cache
+================
+
+`dotnet-sql-cache` is a command line tool that creates table and indexes in Microsoft SQL Server database to be used for distributed caching
+
+### How To Use
+
+Run `dotnet sql-cache --help` for more information about usage.
diff --git a/src/Tools/dotnet-sql-cache/src/Program.cs b/src/Tools/dotnet-sql-cache/src/Program.cs
new file mode 100644
index 0000000000..874244f603
--- /dev/null
+++ b/src/Tools/dotnet-sql-cache/src/Program.cs
@@ -0,0 +1,173 @@
+// Copyright (c) .NET Foundation. 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.Data;
+using System.Data.SqlClient;
+using System.Reflection;
+using Microsoft.Extensions.CommandLineUtils;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.Extensions.Caching.SqlConfig.Tools
+{
+ public class Program
+ {
+ private string _connectionString = null;
+ private string _schemaName = null;
+ private string _tableName = null;
+ private readonly IConsole _console;
+
+ public Program(IConsole console)
+ {
+ Ensure.NotNull(console, nameof(console));
+
+ _console = console;
+ }
+
+ public static int Main(string[] args)
+ {
+ return new Program(PhysicalConsole.Singleton).Run(args);
+ }
+
+ public int Run(string[] args)
+ {
+ DebugHelper.HandleDebugSwitch(ref args);
+
+ try
+ {
+ var app = new CommandLineApplication
+ {
+ Name = "dotnet sql-cache",
+ FullName = "SQL Server Cache Command Line Tool",
+ Description =
+ "Creates table and indexes in Microsoft SQL Server database to be used for distributed caching",
+ };
+
+ app.HelpOption();
+ app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly);
+ var verbose = app.VerboseOption();
+
+ app.Command("create", command =>
+ {
+ command.Description = app.Description;
+
+ var connectionStringArg = command.Argument(
+ "[connectionString]", "The connection string to connect to the database.");
+
+ var schemaNameArg = command.Argument(
+ "[schemaName]", "Name of the table schema.");
+
+ var tableNameArg = command.Argument(
+ "[tableName]", "Name of the table to be created.");
+
+ command.HelpOption();
+
+ command.OnExecute(() =>
+ {
+ var reporter = CreateReporter(verbose.HasValue());
+ if (string.IsNullOrEmpty(connectionStringArg.Value)
+ || string.IsNullOrEmpty(schemaNameArg.Value)
+ || string.IsNullOrEmpty(tableNameArg.Value))
+ {
+ reporter.Error("Invalid input");
+ app.ShowHelp();
+ return 2;
+ }
+
+ _connectionString = connectionStringArg.Value;
+ _schemaName = schemaNameArg.Value;
+ _tableName = tableNameArg.Value;
+
+ return CreateTableAndIndexes(reporter);
+ });
+ });
+
+ // Show help information if no subcommand/option was specified.
+ app.OnExecute(() =>
+ {
+ app.ShowHelp();
+ return 2;
+ });
+
+ return app.Execute(args);
+ }
+ catch (Exception exception)
+ {
+ CreateReporter(verbose: false).Error($"An error occurred. {exception.Message}");
+ return 1;
+ }
+ }
+
+ private IReporter CreateReporter(bool verbose)
+ => new ConsoleReporter(_console, verbose, quiet: false);
+ private int CreateTableAndIndexes(IReporter reporter)
+ {
+ ValidateConnectionString();
+
+ using (var connection = new SqlConnection(_connectionString))
+ {
+ connection.Open();
+
+ var sqlQueries = new SqlQueries(_schemaName, _tableName);
+ var command = new SqlCommand(sqlQueries.TableInfo, connection);
+
+ using (var reader = command.ExecuteReader(CommandBehavior.SingleRow))
+ {
+ if (reader.Read())
+ {
+ reporter.Warn(
+ $"Table with schema '{_schemaName}' and name '{_tableName}' already exists. " +
+ "Provide a different table name and try again.");
+ return 1;
+ }
+ }
+
+ using (var transaction = connection.BeginTransaction())
+ {
+ try
+ {
+ command = new SqlCommand(sqlQueries.CreateTable, connection, transaction);
+
+ reporter.Verbose($"Executing {command.CommandText}");
+ command.ExecuteNonQuery();
+
+ command = new SqlCommand(
+ sqlQueries.CreateNonClusteredIndexOnExpirationTime,
+ connection,
+ transaction);
+
+ reporter.Verbose($"Executing {command.CommandText}");
+ command.ExecuteNonQuery();
+
+ transaction.Commit();
+
+ reporter.Output("Table and index were created successfully.");
+ }
+ catch (Exception ex)
+ {
+ reporter.Error(
+ $"An error occurred while trying to create the table and index. {ex.Message}");
+ transaction.Rollback();
+
+ return 1;
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ private void ValidateConnectionString()
+ {
+ try
+ {
+ new SqlConnectionStringBuilder(_connectionString);
+ }
+ catch (Exception ex)
+ {
+ throw new ArgumentException(
+ $"Invalid SQL Server connection string '{_connectionString}'. {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/src/Tools/dotnet-sql-cache/src/SqlQueries.cs b/src/Tools/dotnet-sql-cache/src/SqlQueries.cs
new file mode 100644
index 0000000000..7b9c73a990
--- /dev/null
+++ b/src/Tools/dotnet-sql-cache/src/SqlQueries.cs
@@ -0,0 +1,67 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.Extensions.Caching.SqlConfig.Tools
+{
+ internal class SqlQueries
+ {
+ private const string CreateTableFormat = "CREATE TABLE {0}(" +
+ // Maximum size of primary key column is 900 bytes (898 bytes from key + 2 additional bytes used by the
+ // Sql Server). In the case where the key is greater than 898 bytes, then it gets truncated.
+ // - Add collation to the key column to make it case-sensitive
+ "Id nvarchar(449) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL, " +
+ "Value varbinary(MAX) NOT NULL, " +
+ "ExpiresAtTime datetimeoffset NOT NULL, " +
+ "SlidingExpirationInSeconds bigint NULL," +
+ "AbsoluteExpiration datetimeoffset NULL, " +
+ "PRIMARY KEY (Id))";
+
+ private const string CreateNonClusteredIndexOnExpirationTimeFormat
+ = "CREATE NONCLUSTERED INDEX Index_ExpiresAtTime ON {0}(ExpiresAtTime)";
+
+ private const string TableInfoFormat =
+ "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE " +
+ "FROM INFORMATION_SCHEMA.TABLES " +
+ "WHERE TABLE_SCHEMA = '{0}' " +
+ "AND TABLE_NAME = '{1}'";
+
+ public SqlQueries(string schemaName, string tableName)
+ {
+ if (string.IsNullOrEmpty(schemaName))
+ {
+ throw new ArgumentException("Schema name cannot be empty or null");
+ }
+ if (string.IsNullOrEmpty(tableName))
+ {
+ throw new ArgumentException("Table name cannot be empty or null");
+ }
+
+ var tableNameWithSchema = string.Format(
+ "{0}.{1}", DelimitIdentifier(schemaName), DelimitIdentifier(tableName));
+ CreateTable = string.Format(CreateTableFormat, tableNameWithSchema);
+ CreateNonClusteredIndexOnExpirationTime = string.Format(
+ CreateNonClusteredIndexOnExpirationTimeFormat,
+ tableNameWithSchema);
+ TableInfo = string.Format(TableInfoFormat, EscapeLiteral(schemaName), EscapeLiteral(tableName));
+ }
+
+ public string CreateTable { get; }
+
+ public string CreateNonClusteredIndexOnExpirationTime { get; }
+
+ public string TableInfo { get; }
+
+ // From EF's SqlServerQuerySqlGenerator
+ private string DelimitIdentifier(string identifier)
+ {
+ return "[" + identifier.Replace("]", "]]") + "]";
+ }
+
+ private string EscapeLiteral(string literal)
+ {
+ return literal.Replace("'", "''");
+ }
+ }
+}
diff --git a/src/Tools/dotnet-sql-cache/src/dotnet-sql-cache.csproj b/src/Tools/dotnet-sql-cache/src/dotnet-sql-cache.csproj
new file mode 100644
index 0000000000..3856c4877c
--- /dev/null
+++ b/src/Tools/dotnet-sql-cache/src/dotnet-sql-cache.csproj
@@ -0,0 +1,30 @@
+
+
+
+ netcoreapp2.1
+ exe
+ Command line tool to create tables and indexes in a Microsoft SQL Server database for distributed caching.
+ cache;distributedcache;sqlserver
+ true
+
+ win-x64;win-x86
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Tools/dotnet-user-secrets/README.md b/src/Tools/dotnet-user-secrets/README.md
new file mode 100644
index 0000000000..0d8666cdb6
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/README.md
@@ -0,0 +1,8 @@
+dotnet-user-secrets
+===================
+
+`dotnet-user-secrets` is a command line tool for managing the secrets in a user secret store.
+
+### How To Use
+
+Run `dotnet user-secrets --help` for more information about usage.
diff --git a/src/Tools/dotnet-user-secrets/src/CommandLineOptions.cs b/src/Tools/dotnet-user-secrets/src/CommandLineOptions.cs
new file mode 100644
index 0000000000..6ce543cc00
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/CommandLineOptions.cs
@@ -0,0 +1,72 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Reflection;
+using Microsoft.Extensions.CommandLineUtils;
+using Microsoft.Extensions.SecretManager.Tools.Internal;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.Extensions.SecretManager.Tools
+{
+ public class CommandLineOptions
+ {
+ public ICommand Command { get; set; }
+ public string Configuration { get; private set; }
+ public string Id { get; private set; }
+ public bool IsHelp { get; private set; }
+ public bool IsVerbose { get; private set; }
+ public string Project { get; private set; }
+
+ public static CommandLineOptions Parse(string[] args, IConsole console)
+ {
+ var app = new CommandLineApplication()
+ {
+ Out = console.Out,
+ Error = console.Error,
+ Name = "dotnet user-secrets",
+ FullName = "User Secrets Manager",
+ Description = "Manages user secrets"
+ };
+
+ app.HelpOption();
+ app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly);
+
+ var optionVerbose = app.VerboseOption();
+
+ var optionProject = app.Option("-p|--project ", "Path to project. Defaults to searching the current directory.",
+ CommandOptionType.SingleValue, inherited: true);
+
+ var optionConfig = app.Option("-c|--configuration ", $"The project configuration to use. Defaults to 'Debug'.",
+ CommandOptionType.SingleValue, inherited: true);
+
+ // the escape hatch if project evaluation fails, or if users want to alter a secret store other than the one
+ // in the current project
+ var optionId = app.Option("--id", "The user secret ID to use.",
+ CommandOptionType.SingleValue, inherited: true);
+
+ var options = new CommandLineOptions();
+
+ app.Command("set", c => SetCommand.Configure(c, options, console));
+ app.Command("remove", c => RemoveCommand.Configure(c, options));
+ app.Command("list", c => ListCommand.Configure(c, options));
+ app.Command("clear", c => ClearCommand.Configure(c, options));
+
+ // Show help information if no subcommand/option was specified.
+ app.OnExecute(() => app.ShowHelp());
+
+ if (app.Execute(args) != 0)
+ {
+ // when command line parsing error in subcommand
+ return null;
+ }
+
+ options.Configuration = optionConfig.Value();
+ options.Id = optionId.Value();
+ options.IsHelp = app.IsShowingInformation;
+ options.IsVerbose = optionVerbose.HasValue();
+ options.Project = optionProject.Value();
+
+ return options;
+ }
+ }
+}
diff --git a/src/Tools/dotnet-user-secrets/src/Internal/ClearCommand.cs b/src/Tools/dotnet-user-secrets/src/Internal/ClearCommand.cs
new file mode 100644
index 0000000000..108fd542d7
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/Internal/ClearCommand.cs
@@ -0,0 +1,27 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.Extensions.CommandLineUtils;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ internal class ClearCommand : ICommand
+ {
+ public static void Configure(CommandLineApplication command, CommandLineOptions options)
+ {
+ command.Description = "Deletes all the application secrets";
+ command.HelpOption();
+
+ command.OnExecute(() =>
+ {
+ options.Command = new ClearCommand();
+ });
+ }
+
+ public void Execute(CommandContext context)
+ {
+ context.SecretStore.Clear();
+ context.SecretStore.Save();
+ }
+ }
+}
diff --git a/src/Tools/dotnet-user-secrets/src/Internal/CommandContext.cs b/src/Tools/dotnet-user-secrets/src/Internal/CommandContext.cs
new file mode 100644
index 0000000000..0ac0ea40fe
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/Internal/CommandContext.cs
@@ -0,0 +1,24 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ public class CommandContext
+ {
+ public CommandContext(
+ SecretsStore store,
+ IReporter reporter,
+ IConsole console)
+ {
+ SecretStore = store;
+ Reporter = reporter;
+ Console = console;
+ }
+
+ public IConsole Console { get; }
+ public IReporter Reporter { get; }
+ public SecretsStore SecretStore { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Tools/dotnet-user-secrets/src/Internal/ICommand.cs b/src/Tools/dotnet-user-secrets/src/Internal/ICommand.cs
new file mode 100644
index 0000000000..636c08a07c
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/Internal/ICommand.cs
@@ -0,0 +1,10 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ public interface ICommand
+ {
+ void Execute(CommandContext context);
+ }
+}
\ No newline at end of file
diff --git a/src/Tools/dotnet-user-secrets/src/Internal/ListCommand.cs b/src/Tools/dotnet-user-secrets/src/Internal/ListCommand.cs
new file mode 100644
index 0000000000..bbefab2a13
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/Internal/ListCommand.cs
@@ -0,0 +1,67 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.Extensions.CommandLineUtils;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ internal class ListCommand : ICommand
+ {
+ private readonly bool _jsonOutput;
+
+ public static void Configure(CommandLineApplication command, CommandLineOptions options)
+ {
+ command.Description = "Lists all the application secrets";
+ command.HelpOption();
+
+ var optJson = command.Option("--json", "Use json output. JSON is wrapped by '//BEGIN' and '//END'",
+ CommandOptionType.NoValue);
+
+ command.OnExecute(() =>
+ {
+ options.Command = new ListCommand(optJson.HasValue());
+ });
+ }
+
+ public ListCommand(bool jsonOutput)
+ {
+ _jsonOutput = jsonOutput;
+ }
+
+ public void Execute(CommandContext context)
+ {
+ if (_jsonOutput)
+ {
+ ReportJson(context);
+ return;
+ }
+
+ if (context.SecretStore.Count == 0)
+ {
+ context.Reporter.Output(Resources.Error_No_Secrets_Found);
+ }
+ else
+ {
+ foreach (var secret in context.SecretStore.AsEnumerable())
+ {
+ context.Reporter.Output(Resources.FormatMessage_Secret_Value_Format(secret.Key, secret.Value));
+ }
+ }
+ }
+
+ private void ReportJson(CommandContext context)
+ {
+ var jObject = new JObject();
+ foreach(var item in context.SecretStore.AsEnumerable())
+ {
+ jObject[item.Key] = item.Value;
+ }
+
+ context.Reporter.Output("//BEGIN");
+ context.Reporter.Output(jObject.ToString(Formatting.Indented));
+ context.Reporter.Output("//END");
+ }
+ }
+}
diff --git a/src/Tools/dotnet-user-secrets/src/Internal/MsBuildProjectFinder.cs b/src/Tools/dotnet-user-secrets/src/Internal/MsBuildProjectFinder.cs
new file mode 100644
index 0000000000..a24843b04d
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/Internal/MsBuildProjectFinder.cs
@@ -0,0 +1,58 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Linq;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ internal class MsBuildProjectFinder
+ {
+ private readonly string _directory;
+
+ public MsBuildProjectFinder(string directory)
+ {
+ Ensure.NotNullOrEmpty(directory, nameof(directory));
+
+ _directory = directory;
+ }
+
+ public string FindMsBuildProject(string project)
+ {
+ var projectPath = project ?? _directory;
+
+ if (!Path.IsPathRooted(projectPath))
+ {
+ projectPath = Path.Combine(_directory, projectPath);
+ }
+
+ if (Directory.Exists(projectPath))
+ {
+ var projects = Directory.EnumerateFileSystemEntries(projectPath, "*.*proj", SearchOption.TopDirectoryOnly)
+ .Where(f => !".xproj".Equals(Path.GetExtension(f), StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ if (projects.Count > 1)
+ {
+ throw new FileNotFoundException(Resources.FormatError_MultipleProjectsFound(projectPath));
+ }
+
+ if (projects.Count == 0)
+ {
+ throw new FileNotFoundException(Resources.FormatError_NoProjectsFound(projectPath));
+ }
+
+ return projects[0];
+ }
+
+ if (!File.Exists(projectPath))
+ {
+ throw new FileNotFoundException(Resources.FormatError_ProjectPath_NotFound(projectPath));
+ }
+
+ return projectPath;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Tools/dotnet-user-secrets/src/Internal/ProjectIdResolver.cs b/src/Tools/dotnet-user-secrets/src/Internal/ProjectIdResolver.cs
new file mode 100644
index 0000000000..da5111ca51
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/Internal/ProjectIdResolver.cs
@@ -0,0 +1,124 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Extensions.CommandLineUtils;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ public class ProjectIdResolver
+ {
+ private const string DefaultConfig = "Debug";
+ private readonly IReporter _reporter;
+ private readonly string _targetsFile;
+ private readonly string _workingDirectory;
+
+ public ProjectIdResolver(IReporter reporter, string workingDirectory)
+ {
+ _workingDirectory = workingDirectory;
+ _reporter = reporter;
+ _targetsFile = FindTargetsFile();
+ }
+
+ public string Resolve(string project, string configuration)
+ {
+ var finder = new MsBuildProjectFinder(_workingDirectory);
+ var projectFile = finder.FindMsBuildProject(project);
+
+ _reporter.Verbose(Resources.FormatMessage_Project_File_Path(projectFile));
+
+ configuration = !string.IsNullOrEmpty(configuration)
+ ? configuration
+ : DefaultConfig;
+
+ var outputFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ try
+ {
+ var args = new[]
+ {
+ "msbuild",
+ projectFile,
+ "/nologo",
+ "/t:_ExtractUserSecretsMetadata", // defined in SecretManager.targets
+ "/p:_UserSecretsMetadataFile=" + outputFile,
+ "/p:Configuration=" + configuration,
+ "/p:CustomAfterMicrosoftCommonTargets=" + _targetsFile,
+ "/p:CustomAfterMicrosoftCommonCrossTargetingTargets=" + _targetsFile,
+ };
+ var psi = new ProcessStartInfo
+ {
+ FileName = DotNetMuxer.MuxerPathOrDefault(),
+ Arguments = ArgumentEscaper.EscapeAndConcatenate(args),
+ RedirectStandardOutput = true,
+ RedirectStandardError = true
+ };
+
+#if DEBUG
+ _reporter.Verbose($"Invoking '{psi.FileName} {psi.Arguments}'");
+#endif
+
+ var process = Process.Start(psi);
+ process.WaitForExit();
+
+ if (process.ExitCode != 0)
+ {
+ _reporter.Verbose(process.StandardOutput.ReadToEnd());
+ _reporter.Verbose(process.StandardError.ReadToEnd());
+ throw new InvalidOperationException(Resources.FormatError_ProjectFailedToLoad(projectFile));
+ }
+
+ var id = File.ReadAllText(outputFile)?.Trim();
+ if (string.IsNullOrEmpty(id))
+ {
+ throw new InvalidOperationException(Resources.FormatError_ProjectMissingId(projectFile));
+ }
+ return id;
+
+ }
+ finally
+ {
+ TryDelete(outputFile);
+ }
+ }
+
+ private string FindTargetsFile()
+ {
+ var assemblyDir = Path.GetDirectoryName(typeof(ProjectIdResolver).Assembly.Location);
+ var searchPaths = new[]
+ {
+ Path.Combine(AppContext.BaseDirectory, "assets"),
+ Path.Combine(assemblyDir, "assets"),
+ AppContext.BaseDirectory,
+ assemblyDir,
+ };
+
+ var targetPath = searchPaths.Select(p => Path.Combine(p, "SecretManager.targets")).FirstOrDefault(File.Exists);
+ if (targetPath == null)
+ {
+ _reporter.Error("Fatal error: could not find SecretManager.targets");
+ return null;
+ }
+ return targetPath;
+ }
+
+ private static void TryDelete(string file)
+ {
+ try
+ {
+ if (File.Exists(file))
+ {
+ File.Delete(file);
+ }
+ }
+ catch
+ {
+ // whatever
+ }
+ }
+ }
+}
diff --git a/src/Tools/dotnet-user-secrets/src/Internal/ReadableJsonConfigurationSource.cs b/src/Tools/dotnet-user-secrets/src/Internal/ReadableJsonConfigurationSource.cs
new file mode 100644
index 0000000000..96d0554aeb
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/Internal/ReadableJsonConfigurationSource.cs
@@ -0,0 +1,18 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using Microsoft.Extensions.Configuration.Json;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ public class ReadableJsonConfigurationProvider : JsonConfigurationProvider
+ {
+ public ReadableJsonConfigurationProvider()
+ : base(new JsonConfigurationSource())
+ {
+ }
+
+ public IDictionary CurrentData => Data;
+ }
+}
\ No newline at end of file
diff --git a/src/Tools/dotnet-user-secrets/src/Internal/RemoveCommand.cs b/src/Tools/dotnet-user-secrets/src/Internal/RemoveCommand.cs
new file mode 100644
index 0000000000..3bc594b149
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/Internal/RemoveCommand.cs
@@ -0,0 +1,48 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.Extensions.CommandLineUtils;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ internal class RemoveCommand : ICommand
+ {
+ private readonly string _keyName;
+
+ public static void Configure(CommandLineApplication command, CommandLineOptions options)
+ {
+ command.Description = "Removes the specified user secret";
+ command.HelpOption();
+
+ var keyArg = command.Argument("[name]", "Name of the secret");
+ command.OnExecute(() =>
+ {
+ if (keyArg.Value == null)
+ {
+ throw new CommandParsingException(command, Resources.FormatError_MissingArgument("name"));
+ }
+
+ options.Command = new RemoveCommand(keyArg.Value);
+ });
+ }
+
+
+ public RemoveCommand(string keyName)
+ {
+ _keyName = keyName;
+ }
+
+ public void Execute(CommandContext context)
+ {
+ if (!context.SecretStore.ContainsKey(_keyName))
+ {
+ context.Reporter.Warn(Resources.FormatError_Missing_Secret(_keyName));
+ }
+ else
+ {
+ context.SecretStore.Remove(_keyName);
+ context.SecretStore.Save();
+ }
+ }
+ }
+}
diff --git a/src/Tools/dotnet-user-secrets/src/Internal/SecretsStore.cs b/src/Tools/dotnet-user-secrets/src/Internal/SecretsStore.cs
new file mode 100644
index 0000000000..93d46242f2
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/Internal/SecretsStore.cs
@@ -0,0 +1,87 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Configuration.UserSecrets;
+using Microsoft.Extensions.Tools.Internal;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ public class SecretsStore
+ {
+ private readonly string _secretsFilePath;
+ private IDictionary _secrets;
+
+ public SecretsStore(string userSecretsId, IReporter reporter)
+ {
+ Ensure.NotNull(userSecretsId, nameof(userSecretsId));
+
+ _secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId);
+
+ // workaround bug in configuration
+ var secretDir = Path.GetDirectoryName(_secretsFilePath);
+ Directory.CreateDirectory(secretDir);
+
+ reporter.Verbose(Resources.FormatMessage_Secret_File_Path(_secretsFilePath));
+ _secrets = Load(userSecretsId);
+ }
+
+ public string this[string key]
+ {
+ get
+ {
+ return _secrets[key];
+ }
+ }
+
+ public int Count => _secrets.Count;
+
+ public bool ContainsKey(string key) => _secrets.ContainsKey(key);
+
+ public IEnumerable> AsEnumerable() => _secrets;
+
+ public void Clear() => _secrets.Clear();
+
+ public void Set(string key, string value) => _secrets[key] = value;
+
+ public void Remove(string key)
+ {
+ if (_secrets.ContainsKey(key))
+ {
+ _secrets.Remove(key);
+ }
+ }
+
+ public virtual void Save()
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(_secretsFilePath));
+
+ var contents = new JObject();
+ if (_secrets != null)
+ {
+ foreach (var secret in _secrets.AsEnumerable())
+ {
+ contents[secret.Key] = secret.Value;
+ }
+ }
+
+ File.WriteAllText(_secretsFilePath, contents.ToString(), Encoding.UTF8);
+ }
+
+ protected virtual IDictionary Load(string userSecretsId)
+ {
+ return new ConfigurationBuilder()
+ .AddJsonFile(_secretsFilePath, optional: true)
+ .Build()
+ .AsEnumerable()
+ .Where(i => i.Value != null)
+ .ToDictionary(i => i.Key, i => i.Value, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Tools/dotnet-user-secrets/src/Internal/SetCommand.cs b/src/Tools/dotnet-user-secrets/src/Internal/SetCommand.cs
new file mode 100644
index 0000000000..fa389ca242
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/Internal/SetCommand.cs
@@ -0,0 +1,106 @@
+// Copyright (c) .NET 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.Runtime.InteropServices;
+using System.Text;
+using Microsoft.Extensions.CommandLineUtils;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ internal class SetCommand
+ {
+ public static void Configure(CommandLineApplication command, CommandLineOptions options, IConsole console)
+ {
+ command.Description = "Sets the user secret to the specified value";
+ command.ExtendedHelpText = @"
+Additional Info:
+ This command will also handle piped input. Piped input is expected to be a valid JSON format.
+
+Examples:
+ dotnet user-secrets set ConnStr ""User ID=bob;Password=***""
+";
+
+ var catCmd = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? @"type .\secrets.json"
+ : "cat ./secrets.json";
+
+ command.ExtendedHelpText += $@" {catCmd} | dotnet user-secrets set";
+
+ command.HelpOption();
+
+ var nameArg = command.Argument("[name]", "Name of the secret");
+ var valueArg = command.Argument("[value]", "Value of the secret");
+
+ command.OnExecute(() =>
+ {
+ if (console.IsInputRedirected && nameArg.Value == null)
+ {
+ options.Command = new FromStdInStrategy();
+ }
+ else
+ {
+ if (string.IsNullOrEmpty(nameArg.Value))
+ {
+ throw new CommandParsingException(command, Resources.FormatError_MissingArgument("name"));
+ }
+
+ if (valueArg.Value == null)
+ {
+ throw new CommandParsingException(command, Resources.FormatError_MissingArgument("value"));
+ }
+
+ options.Command = new ForOneValueStrategy(nameArg.Value, valueArg.Value);
+ }
+ });
+ }
+
+ public class FromStdInStrategy : ICommand
+ {
+ public void Execute(CommandContext context)
+ {
+ // parses stdin with the same parser that Microsoft.Extensions.Configuration.Json would use
+ var provider = new ReadableJsonConfigurationProvider();
+ using (var stream = new MemoryStream())
+ {
+ using (var writer = new StreamWriter(stream, Encoding.Unicode, 1024, true))
+ {
+ writer.Write(context.Console.In.ReadToEnd()); // TODO buffer?
+ }
+
+ stream.Seek(0, SeekOrigin.Begin);
+ provider.Load(stream);
+ }
+
+ foreach (var k in provider.CurrentData)
+ {
+ context.SecretStore.Set(k.Key, k.Value);
+ }
+
+ context.Reporter.Output(Resources.FormatMessage_Saved_Secrets(provider.CurrentData.Count));
+
+ context.SecretStore.Save();
+ }
+ }
+
+ public class ForOneValueStrategy : ICommand
+ {
+ private readonly string _keyName;
+ private readonly string _keyValue;
+
+ public ForOneValueStrategy(string keyName, string keyValue)
+ {
+ _keyName = keyName;
+ _keyValue = keyValue;
+ }
+
+ public void Execute(CommandContext context)
+ {
+ context.SecretStore.Set(_keyName, _keyValue);
+ context.SecretStore.Save();
+ context.Reporter.Output(Resources.FormatMessage_Saved_Secret(_keyName, _keyValue));
+ }
+ }
+ }
+}
diff --git a/src/Tools/dotnet-user-secrets/src/Program.cs b/src/Tools/dotnet-user-secrets/src/Program.cs
new file mode 100644
index 0000000000..187e40128f
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/Program.cs
@@ -0,0 +1,105 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using Microsoft.Extensions.CommandLineUtils;
+using Microsoft.Extensions.SecretManager.Tools.Internal;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.Extensions.SecretManager.Tools
+{
+ public class Program
+ {
+ private readonly IConsole _console;
+ private readonly string _workingDirectory;
+
+ public static int Main(string[] args)
+ {
+ DebugHelper.HandleDebugSwitch(ref args);
+
+ int rc;
+ new Program(PhysicalConsole.Singleton, Directory.GetCurrentDirectory()).TryRun(args, out rc);
+ return rc;
+ }
+
+ public Program(IConsole console, string workingDirectory)
+ {
+ _console = console;
+ _workingDirectory = workingDirectory;
+ }
+
+ public bool TryRun(string[] args, out int returnCode)
+ {
+ try
+ {
+ returnCode = RunInternal(args);
+ return true;
+ }
+ catch (Exception exception)
+ {
+ var reporter = CreateReporter(verbose: true);
+ reporter.Verbose(exception.ToString());
+ reporter.Error(Resources.FormatError_Command_Failed(exception.Message));
+ returnCode = 1;
+ return false;
+ }
+ }
+
+ internal int RunInternal(params string[] args)
+ {
+ CommandLineOptions options;
+ try
+ {
+ options = CommandLineOptions.Parse(args, _console);
+ }
+ catch (CommandParsingException ex)
+ {
+ CreateReporter(verbose: false).Error(ex.Message);
+ return 1;
+ }
+
+ if (options == null)
+ {
+ return 1;
+ }
+
+ if (options.IsHelp)
+ {
+ return 2;
+ }
+
+ var reporter = CreateReporter(options.IsVerbose);
+
+ string userSecretsId;
+ try
+ {
+ userSecretsId = ResolveId(options, reporter);
+ }
+ catch (Exception ex) when (ex is InvalidOperationException || ex is FileNotFoundException)
+ {
+ reporter.Error(ex.Message);
+ return 1;
+ }
+
+ var store = new SecretsStore(userSecretsId, reporter);
+ var context = new Internal.CommandContext(store, reporter, _console);
+ options.Command.Execute(context);
+ return 0;
+ }
+
+ private IReporter CreateReporter(bool verbose)
+ => new ConsoleReporter(_console, verbose, quiet: false);
+
+ internal string ResolveId(CommandLineOptions options, IReporter reporter)
+ {
+ if (!string.IsNullOrEmpty(options.Id))
+ {
+ return options.Id;
+ }
+
+ var resolver = new ProjectIdResolver(reporter, _workingDirectory);
+ return resolver.Resolve(options.Project, options.Configuration);
+ }
+ }
+}
diff --git a/src/Tools/dotnet-user-secrets/src/Properties/AssemblyInfo.cs b/src/Tools/dotnet-user-secrets/src/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..ae77d384c1
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.Extensions.SecretManager.Tools.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/Tools/dotnet-user-secrets/src/Properties/Resources.Designer.cs b/src/Tools/dotnet-user-secrets/src/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..a75fc0108f
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/Properties/Resources.Designer.cs
@@ -0,0 +1,256 @@
+//
+namespace Microsoft.Extensions.SecretManager.Tools
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.Extensions.SecretManager.Tools.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ ///
+ /// Command failed : {message}
+ ///
+ internal static string Error_Command_Failed
+ {
+ get { return GetString("Error_Command_Failed"); }
+ }
+
+ ///
+ /// Command failed : {message}
+ ///
+ internal static string FormatError_Command_Failed(object message)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Error_Command_Failed", "message"), message);
+ }
+
+ ///
+ /// Missing parameter value for '{name}'.
+ /// Use the '--help' flag to see info.
+ ///
+ internal static string Error_MissingArgument
+ {
+ get { return GetString("Error_MissingArgument"); }
+ }
+
+ ///
+ /// Missing parameter value for '{name}'.
+ /// Use the '--help' flag to see info.
+ ///
+ internal static string FormatError_MissingArgument(object name)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Error_MissingArgument", "name"), name);
+ }
+
+ ///
+ /// Cannot find '{key}' in the secret store.
+ ///
+ internal static string Error_Missing_Secret
+ {
+ get { return GetString("Error_Missing_Secret"); }
+ }
+
+ ///
+ /// Cannot find '{key}' in the secret store.
+ ///
+ internal static string FormatError_Missing_Secret(object key)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Error_Missing_Secret", "key"), key);
+ }
+
+ ///
+ /// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.
+ ///
+ internal static string Error_MultipleProjectsFound
+ {
+ get { return GetString("Error_MultipleProjectsFound"); }
+ }
+
+ ///
+ /// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.
+ ///
+ internal static string FormatError_MultipleProjectsFound(object projectPath)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Error_MultipleProjectsFound", "projectPath"), projectPath);
+ }
+
+ ///
+ /// No secrets configured for this application.
+ ///
+ internal static string Error_No_Secrets_Found
+ {
+ get { return GetString("Error_No_Secrets_Found"); }
+ }
+
+ ///
+ /// No secrets configured for this application.
+ ///
+ internal static string FormatError_No_Secrets_Found()
+ {
+ return GetString("Error_No_Secrets_Found");
+ }
+
+ ///
+ /// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.
+ ///
+ internal static string Error_NoProjectsFound
+ {
+ get { return GetString("Error_NoProjectsFound"); }
+ }
+
+ ///
+ /// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.
+ ///
+ internal static string FormatError_NoProjectsFound(object projectPath)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Error_NoProjectsFound", "projectPath"), projectPath);
+ }
+
+ ///
+ /// Could not find the global property 'UserSecretsId' in MSBuild project '{project}'. Ensure this property is set in the project or use the '--id' command line option.
+ ///
+ internal static string Error_ProjectMissingId
+ {
+ get { return GetString("Error_ProjectMissingId"); }
+ }
+
+ ///
+ /// Could not find the global property 'UserSecretsId' in MSBuild project '{project}'. Ensure this property is set in the project or use the '--id' command line option.
+ ///
+ internal static string FormatError_ProjectMissingId(object project)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectMissingId", "project"), project);
+ }
+
+ ///
+ /// The project file '{path}' does not exist.
+ ///
+ internal static string Error_ProjectPath_NotFound
+ {
+ get { return GetString("Error_ProjectPath_NotFound"); }
+ }
+
+ ///
+ /// The project file '{path}' does not exist.
+ ///
+ internal static string FormatError_ProjectPath_NotFound(object path)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectPath_NotFound", "path"), path);
+ }
+
+ ///
+ /// Could not load the MSBuild project '{project}'.
+ ///
+ internal static string Error_ProjectFailedToLoad
+ {
+ get { return GetString("Error_ProjectFailedToLoad"); }
+ }
+
+ ///
+ /// Could not load the MSBuild project '{project}'.
+ ///
+ internal static string FormatError_ProjectFailedToLoad(object project)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectFailedToLoad", "project"), project);
+ }
+
+ ///
+ /// Project file path {project}.
+ ///
+ internal static string Message_Project_File_Path
+ {
+ get { return GetString("Message_Project_File_Path"); }
+ }
+
+ ///
+ /// Project file path {project}.
+ ///
+ internal static string FormatMessage_Project_File_Path(object project)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Message_Project_File_Path", "project"), project);
+ }
+
+ ///
+ /// Successfully saved {key} = {value} to the secret store.
+ ///
+ internal static string Message_Saved_Secret
+ {
+ get { return GetString("Message_Saved_Secret"); }
+ }
+
+ ///
+ /// Successfully saved {key} = {value} to the secret store.
+ ///
+ internal static string FormatMessage_Saved_Secret(object key, object value)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secret", "key", "value"), key, value);
+ }
+
+ ///
+ /// Successfully saved {number} secrets to the secret store.
+ ///
+ internal static string Message_Saved_Secrets
+ {
+ get { return GetString("Message_Saved_Secrets"); }
+ }
+
+ ///
+ /// Successfully saved {number} secrets to the secret store.
+ ///
+ internal static string FormatMessage_Saved_Secrets(object number)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secrets", "number"), number);
+ }
+
+ ///
+ /// Secrets file path {secretsFilePath}.
+ ///
+ internal static string Message_Secret_File_Path
+ {
+ get { return GetString("Message_Secret_File_Path"); }
+ }
+
+ ///
+ /// Secrets file path {secretsFilePath}.
+ ///
+ internal static string FormatMessage_Secret_File_Path(object secretsFilePath)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Message_Secret_File_Path", "secretsFilePath"), secretsFilePath);
+ }
+
+ ///
+ /// {key} = {value}
+ ///
+ internal static string Message_Secret_Value_Format
+ {
+ get { return GetString("Message_Secret_Value_Format"); }
+ }
+
+ ///
+ /// {key} = {value}
+ ///
+ internal static string FormatMessage_Secret_Value_Format(object key, object value)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Message_Secret_Value_Format", "key", "value"), key, value);
+ }
+
+ private static string GetString(string name, params string[] formatterNames)
+ {
+ var value = _resourceManager.GetString(name);
+
+ System.Diagnostics.Debug.Assert(value != null);
+
+ if (formatterNames != null)
+ {
+ for (var i = 0; i < formatterNames.Length; i++)
+ {
+ value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
+ }
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/Tools/dotnet-user-secrets/src/Resources.resx b/src/Tools/dotnet-user-secrets/src/Resources.resx
new file mode 100644
index 0000000000..c9222930fc
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/Resources.resx
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Command failed : {message}
+
+
+ Missing parameter value for '{name}'.
+Use the '--help' flag to see info.
+
+
+ Cannot find '{key}' in the secret store.
+
+
+ Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.
+
+
+ No secrets configured for this application.
+
+
+ Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.
+
+
+ Could not find the global property 'UserSecretsId' in MSBuild project '{project}'. Ensure this property is set in the project or use the '--id' command line option.
+
+
+ The project file '{path}' does not exist.
+
+
+ Could not load the MSBuild project '{project}'.
+
+
+ Project file path {project}.
+
+
+ Successfully saved {key} = {value} to the secret store.
+
+
+ Successfully saved {number} secrets to the secret store.
+
+
+ Secrets file path {secretsFilePath}.
+
+
+ {key} = {value}
+
+
\ No newline at end of file
diff --git a/src/Tools/dotnet-user-secrets/src/assets/SecretManager.targets b/src/Tools/dotnet-user-secrets/src/assets/SecretManager.targets
new file mode 100644
index 0000000000..8cf63eac00
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/assets/SecretManager.targets
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj b/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj
new file mode 100644
index 0000000000..a66a31dd7e
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj
@@ -0,0 +1,42 @@
+
+
+
+ netcoreapp2.1
+ exe
+ Command line tool to manage user secrets for Microsoft.Extensions.Configuration.
+ false
+ Microsoft.Extensions.SecretManager.Tools
+ configuration;secrets;usersecrets
+ true
+
+ win-x64;win-x86
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Tools/dotnet-user-secrets/test/MsBuildProjectFinderTest.cs b/src/Tools/dotnet-user-secrets/test/MsBuildProjectFinderTest.cs
new file mode 100644
index 0000000000..6e7a290834
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/test/MsBuildProjectFinderTest.cs
@@ -0,0 +1,85 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.IO;
+using Microsoft.Extensions.SecretManager.Tools.Internal;
+using Xunit;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Tests
+{
+ public class MsBuildProjectFinderTest
+ {
+ [Theory]
+ [InlineData(".csproj")]
+ [InlineData(".vbproj")]
+ [InlineData(".fsproj")]
+ public void FindsSingleProject(string extension)
+ {
+ using (var files = new TemporaryFileProvider())
+ {
+ var filename = "TestProject" + extension;
+ files.Add(filename, "");
+
+ var finder = new MsBuildProjectFinder(files.Root);
+
+ Assert.Equal(Path.Combine(files.Root, filename), finder.FindMsBuildProject(null));
+ }
+ }
+
+ [Fact]
+ public void ThrowsWhenNoFile()
+ {
+ using (var files = new TemporaryFileProvider())
+ {
+ var finder = new MsBuildProjectFinder(files.Root);
+
+ Assert.Throws(() => finder.FindMsBuildProject(null));
+ }
+ }
+
+ [Fact]
+ public void DoesNotMatchXproj()
+ {
+ using (var files = new TemporaryFileProvider())
+ {
+ var finder = new MsBuildProjectFinder(files.Root);
+ files.Add("test.xproj", "");
+
+ Assert.Throws(() => finder.FindMsBuildProject(null));
+ }
+ }
+
+ [Fact]
+ public void ThrowsWhenMultipleFile()
+ {
+ using (var files = new TemporaryFileProvider())
+ {
+ files.Add("Test1.csproj", "");
+ files.Add("Test2.csproj", "");
+ var finder = new MsBuildProjectFinder(files.Root);
+
+ Assert.Throws(() => finder.FindMsBuildProject(null));
+ }
+ }
+
+ [Fact]
+ public void ThrowsWhenFileDoesNotExist()
+ {
+ using (var files = new TemporaryFileProvider())
+ {
+ var finder = new MsBuildProjectFinder(files.Root);
+
+ Assert.Throws(() => finder.FindMsBuildProject("test.csproj"));
+ }
+ }
+
+ [Fact]
+ public void ThrowsWhenRootDoesNotExist()
+ {
+ var files = new TemporaryFileProvider();
+ var finder = new MsBuildProjectFinder(files.Root);
+ files.Dispose();
+ Assert.Throws(() => finder.FindMsBuildProject(null));
+ }
+ }
+}
diff --git a/src/Tools/dotnet-user-secrets/test/SecretManagerTests.cs b/src/Tools/dotnet-user-secrets/test/SecretManagerTests.cs
new file mode 100644
index 0000000000..ee7ca55247
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/test/SecretManagerTests.cs
@@ -0,0 +1,327 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Microsoft.Extensions.Configuration.UserSecrets;
+using Microsoft.Extensions.Configuration.UserSecrets.Tests;
+using Microsoft.Extensions.Tools.Internal;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Tests
+{
+ public class SecretManagerTests : IClassFixture
+ {
+ private readonly TestConsole _console;
+ private readonly UserSecretsTestFixture _fixture;
+ private readonly StringBuilder _output = new StringBuilder();
+
+ public SecretManagerTests(UserSecretsTestFixture fixture, ITestOutputHelper output)
+ {
+ _fixture = fixture;
+
+ _console = new TestConsole(output)
+ {
+ Error = new StringWriter(_output),
+ Out = new StringWriter(_output),
+ };
+ }
+
+ private Program CreateProgram()
+ {
+ return new Program(_console, Directory.GetCurrentDirectory());
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ public void Error_MissingId(string id)
+ {
+ var project = Path.Combine(_fixture.CreateProject(id), "TestProject.csproj");
+ var secretManager = CreateProgram();
+
+ secretManager.RunInternal("list", "-p", project);
+ Assert.Contains(Resources.FormatError_ProjectMissingId(project), _output.ToString());
+ }
+
+ [Fact]
+ public void Error_InvalidProjectFormat()
+ {
+ var project = Path.Combine(_fixture.CreateProject("<"), "TestProject.csproj");
+ var secretManager = CreateProgram();
+
+ secretManager.RunInternal("list", "-p", project);
+ Assert.Contains(Resources.FormatError_ProjectFailedToLoad(project), _output.ToString());
+ }
+
+ [Fact]
+ public void Error_Project_DoesNotExist()
+ {
+ var projectPath = Path.Combine(_fixture.GetTempSecretProject(), "does_not_exist", "TestProject.csproj");
+ var secretManager = CreateProgram();
+
+ secretManager.RunInternal("list", "--project", projectPath);
+ Assert.Contains(Resources.FormatError_ProjectPath_NotFound(projectPath), _output.ToString());
+ }
+
+ [Fact]
+ public void SupportsRelativePaths()
+ {
+ var projectPath = _fixture.GetTempSecretProject();
+ var cwd = Path.Combine(projectPath, "nested1");
+ Directory.CreateDirectory(cwd);
+ var secretManager = new Program(_console, cwd);
+
+ secretManager.RunInternal("list", "-p", ".." + Path.DirectorySeparatorChar, "--verbose");
+
+ Assert.Contains(Resources.FormatMessage_Project_File_Path(Path.Combine(cwd, "..", "TestProject.csproj")), _output.ToString());
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void SetSecrets(bool fromCurrentDirectory)
+ {
+ var secrets = new KeyValuePair[]
+ {
+ new KeyValuePair("key1", Guid.NewGuid().ToString()),
+ new KeyValuePair("Facebook:AppId", Guid.NewGuid().ToString()),
+ new KeyValuePair(@"key-@\/.~123!#$%^&*())-+==", @"key-@\/.~123!#$%^&*())-+=="),
+ new KeyValuePair("key2", string.Empty)
+ };
+
+ var projectPath = _fixture.GetTempSecretProject();
+ var dir = fromCurrentDirectory
+ ? projectPath
+ : Path.GetTempPath();
+ var secretManager = new Program(_console, dir);
+
+ foreach (var secret in secrets)
+ {
+ var parameters = fromCurrentDirectory ?
+ new string[] { "set", secret.Key, secret.Value } :
+ new string[] { "set", secret.Key, secret.Value, "-p", projectPath };
+ secretManager.RunInternal(parameters);
+ }
+
+ foreach (var keyValue in secrets)
+ {
+ Assert.Contains(
+ string.Format("Successfully saved {0} = {1} to the secret store.", keyValue.Key, keyValue.Value),
+ _output.ToString());
+ }
+
+ _output.Clear();
+ var args = fromCurrentDirectory
+ ? new string[] { "list" }
+ : new string[] { "list", "-p", projectPath };
+ secretManager.RunInternal(args);
+ foreach (var keyValue in secrets)
+ {
+ Assert.Contains(
+ string.Format("{0} = {1}", keyValue.Key, keyValue.Value),
+ _output.ToString());
+ }
+
+ // Remove secrets.
+ _output.Clear();
+ foreach (var secret in secrets)
+ {
+ var parameters = fromCurrentDirectory ?
+ new string[] { "remove", secret.Key } :
+ new string[] { "remove", secret.Key, "-p", projectPath };
+ secretManager.RunInternal(parameters);
+ }
+
+ // Verify secrets are removed.
+ _output.Clear();
+ args = fromCurrentDirectory
+ ? new string[] { "list" }
+ : new string[] { "list", "-p", projectPath };
+ secretManager.RunInternal(args);
+ Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
+ }
+
+ [Fact]
+ public void SetSecret_Update_Existing_Secret()
+ {
+ var projectPath = _fixture.GetTempSecretProject();
+ var secretManager = CreateProgram();
+
+ secretManager.RunInternal("set", "secret1", "value1", "-p", projectPath);
+ Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _output.ToString());
+ secretManager.RunInternal("set", "secret1", "value2", "-p", projectPath);
+ Assert.Contains("Successfully saved secret1 = value2 to the secret store.", _output.ToString());
+
+ _output.Clear();
+
+ secretManager.RunInternal("list", "-p", projectPath);
+ Assert.Contains("secret1 = value2", _output.ToString());
+ }
+
+ [Fact]
+ public void SetSecret_With_Verbose_Flag()
+ {
+ string secretId;
+ var projectPath = _fixture.GetTempSecretProject(out secretId);
+ var secretManager = CreateProgram();
+
+ secretManager.RunInternal("-v", "set", "secret1", "value1", "-p", projectPath);
+ Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _output.ToString());
+ Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _output.ToString());
+ Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _output.ToString());
+ _output.Clear();
+
+ secretManager.RunInternal("-v", "list", "-p", projectPath);
+
+ Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _output.ToString());
+ Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _output.ToString());
+ Assert.Contains("secret1 = value1", _output.ToString());
+ }
+
+ [Fact]
+ public void Remove_Non_Existing_Secret()
+ {
+ var projectPath = _fixture.GetTempSecretProject();
+ var secretManager = CreateProgram();
+ secretManager.RunInternal("remove", "secret1", "-p", projectPath);
+ Assert.Contains("Cannot find 'secret1' in the secret store.", _output.ToString());
+ }
+
+ [Fact]
+ public void Remove_Is_Case_Insensitive()
+ {
+ var projectPath = _fixture.GetTempSecretProject();
+ var secretManager = CreateProgram();
+ secretManager.RunInternal("set", "SeCreT1", "value", "-p", projectPath);
+ secretManager.RunInternal("list", "-p", projectPath);
+ Assert.Contains("SeCreT1 = value", _output.ToString());
+ secretManager.RunInternal("remove", "secret1", "-p", projectPath);
+
+ _output.Clear();
+ secretManager.RunInternal("list", "-p", projectPath);
+
+ Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
+ }
+
+ [Fact]
+ public void List_Flattens_Nested_Objects()
+ {
+ string secretId;
+ var projectPath = _fixture.GetTempSecretProject(out secretId);
+ var secretsFile = PathHelper.GetSecretsPathFromSecretsId(secretId);
+ Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
+ File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
+ var secretManager = CreateProgram();
+ secretManager.RunInternal("list", "-p", projectPath);
+ Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _output.ToString());
+ }
+
+ [Fact]
+ public void List_Json()
+ {
+ string id;
+ var projectPath = _fixture.GetTempSecretProject(out id);
+ var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id);
+ Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
+ File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
+ var secretManager = new Program(_console, Path.GetDirectoryName(projectPath));
+ secretManager.RunInternal("list", "--id", id, "--json");
+ var stdout = _output.ToString();
+ Assert.Contains("//BEGIN", stdout);
+ Assert.Contains(@"""AzureAd:ClientSecret"": ""abcd郩˙î""", stdout);
+ Assert.Contains("//END", stdout);
+ }
+
+ [Fact]
+ public void Set_Flattens_Nested_Objects()
+ {
+ string secretId;
+ var projectPath = _fixture.GetTempSecretProject(out secretId);
+ var secretsFile = PathHelper.GetSecretsPathFromSecretsId(secretId);
+ Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
+ File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
+ var secretManager = CreateProgram();
+ secretManager.RunInternal("set", "AzureAd:ClientSecret", "¡™£¢∞", "-p", projectPath);
+ secretManager.RunInternal("list", "-p", projectPath);
+
+ Assert.Contains("AzureAd:ClientSecret = ¡™£¢∞", _output.ToString());
+ var fileContents = File.ReadAllText(secretsFile, Encoding.UTF8);
+ Assert.Equal(@"{
+ ""AzureAd:ClientSecret"": ""¡™£¢∞""
+}",
+ fileContents, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true);
+ }
+
+ [Fact]
+ public void List_Empty_Secrets_File()
+ {
+ var projectPath = _fixture.GetTempSecretProject();
+ var secretManager = CreateProgram();
+ secretManager.RunInternal("list", "-p", projectPath);
+ Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void Clear_Secrets(bool fromCurrentDirectory)
+ {
+ var projectPath = _fixture.GetTempSecretProject();
+
+ var dir = fromCurrentDirectory
+ ? projectPath
+ : Path.GetTempPath();
+
+ var secretManager = new Program(_console, dir);
+
+ var secrets = new KeyValuePair[]
+ {
+ new KeyValuePair("key1", Guid.NewGuid().ToString()),
+ new KeyValuePair("Facebook:AppId", Guid.NewGuid().ToString()),
+ new KeyValuePair(@"key-@\/.~123!#$%^&*())-+==", @"key-@\/.~123!#$%^&*())-+=="),
+ new KeyValuePair("key2", string.Empty)
+ };
+
+ foreach (var secret in secrets)
+ {
+ var parameters = fromCurrentDirectory ?
+ new string[] { "set", secret.Key, secret.Value } :
+ new string[] { "set", secret.Key, secret.Value, "-p", projectPath };
+ secretManager.RunInternal(parameters);
+ }
+
+ foreach (var keyValue in secrets)
+ {
+ Assert.Contains(
+ string.Format("Successfully saved {0} = {1} to the secret store.", keyValue.Key, keyValue.Value),
+ _output.ToString());
+ }
+
+ // Verify secrets are persisted.
+ _output.Clear();
+ var args = fromCurrentDirectory ?
+ new string[] { "list" } :
+ new string[] { "list", "-p", projectPath };
+ secretManager.RunInternal(args);
+ foreach (var keyValue in secrets)
+ {
+ Assert.Contains(
+ string.Format("{0} = {1}", keyValue.Key, keyValue.Value),
+ _output.ToString());
+ }
+
+ // Clear secrets.
+ _output.Clear();
+ args = fromCurrentDirectory ? new string[] { "clear" } : new string[] { "clear", "-p", projectPath };
+ secretManager.RunInternal(args);
+
+ args = fromCurrentDirectory ? new string[] { "list" } : new string[] { "list", "-p", projectPath };
+ secretManager.RunInternal(args);
+ Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Tools/dotnet-user-secrets/test/SetCommandTest.cs b/src/Tools/dotnet-user-secrets/test/SetCommandTest.cs
new file mode 100644
index 0000000000..99941fad91
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/test/SetCommandTest.cs
@@ -0,0 +1,106 @@
+// Copyright (c) .NET 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.Collections.Generic;
+using Microsoft.Extensions.SecretManager.Tools.Internal;
+using Microsoft.Extensions.Tools.Internal;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Tests
+{
+
+ public class SetCommandTest
+ {
+ private readonly ITestOutputHelper _output;
+
+ public SetCommandTest(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ [Fact]
+ public void SetsFromPipedInput()
+ {
+ var input = @"
+{
+ ""Key1"": ""str value"",
+""Key2"": 1234,
+""Key3"": false
+}";
+ var testConsole = new TestConsole(_output)
+ {
+ IsInputRedirected = true,
+ In = new StringReader(input)
+ };
+ var secretStore = new TestSecretsStore(_output);
+ var command = new SetCommand.FromStdInStrategy();
+
+ command.Execute(new CommandContext(secretStore, new TestReporter(_output), testConsole));
+
+ Assert.Equal(3, secretStore.Count);
+ Assert.Equal("str value", secretStore["Key1"]);
+ Assert.Equal("1234", secretStore["Key2"]);
+ Assert.Equal("False", secretStore["Key3"]);
+ }
+
+ [Fact]
+ public void ParsesNestedObjects()
+ {
+ var input = @"
+ {
+ ""Key1"": {
+ ""nested"" : ""value""
+ },
+ ""array"": [ 1, 2 ]
+ }";
+
+ var testConsole = new TestConsole(_output)
+ {
+ IsInputRedirected = true,
+ In = new StringReader(input)
+ };
+ var secretStore = new TestSecretsStore(_output);
+ var command = new SetCommand.FromStdInStrategy();
+
+ command.Execute(new CommandContext(secretStore, new TestReporter(_output), testConsole));
+
+ Assert.Equal(3, secretStore.Count);
+ Assert.True(secretStore.ContainsKey("Key1:nested"));
+ Assert.Equal("value", secretStore["Key1:nested"]);
+ Assert.Equal("1", secretStore["array:0"]);
+ Assert.Equal("2", secretStore["array:1"]);
+ }
+
+ [Fact]
+ public void OnlyPipesInIfNoArgs()
+ {
+ var testConsole = new TestConsole(_output)
+ {
+ IsInputRedirected = true,
+ In = new StringReader("")
+ };
+ var options = CommandLineOptions.Parse(new [] { "set", "key", "value" }, testConsole);
+ Assert.IsType(options.Command);
+ }
+
+ private class TestSecretsStore : SecretsStore
+ {
+ public TestSecretsStore(ITestOutputHelper output)
+ : base("xyz", new TestReporter(output))
+ {
+ }
+
+ protected override IDictionary Load(string userSecretsId)
+ {
+ return new Dictionary();
+ }
+
+ public override void Save()
+ {
+ // noop
+ }
+ }
+ }
+}
diff --git a/src/Tools/dotnet-user-secrets/test/TemporaryFileProvider.cs b/src/Tools/dotnet-user-secrets/test/TemporaryFileProvider.cs
new file mode 100644
index 0000000000..34c2e8e2ba
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/test/TemporaryFileProvider.cs
@@ -0,0 +1,29 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Text;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Tests
+{
+ internal class TemporaryFileProvider : IDisposable
+ {
+ public TemporaryFileProvider()
+ {
+ Root = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "tmpfiles", Guid.NewGuid().ToString())).FullName;
+ }
+
+ public string Root { get; }
+
+ public void Add(string filename, string contents)
+ {
+ File.WriteAllText(Path.Combine(Root, filename), contents, Encoding.UTF8);
+ }
+
+ public void Dispose()
+ {
+ Directory.Delete(Root, recursive: true);
+ }
+ }
+}
diff --git a/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs b/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs
new file mode 100644
index 0000000000..14e62805bc
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs
@@ -0,0 +1,97 @@
+// Copyright (c) .NET Foundation. 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;
+
+namespace Microsoft.Extensions.Configuration.UserSecrets.Tests
+{
+ public class UserSecretsTestFixture : IDisposable
+ {
+ private Stack _disposables = new Stack();
+
+ public const string TestSecretsId = "b918174fa80346bbb7f4a386729c0eff";
+
+ public UserSecretsTestFixture()
+ {
+ _disposables.Push(() => TryDelete(Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(TestSecretsId))));
+ }
+
+ public void Dispose()
+ {
+ while (_disposables.Count > 0)
+ {
+ _disposables.Pop()?.Invoke();
+ }
+ }
+
+ public string GetTempSecretProject()
+ {
+ string userSecretsId;
+ return GetTempSecretProject(out userSecretsId);
+ }
+
+ private const string ProjectTemplate = @"
+
+ Exe
+ netcoreapp2.1
+ {0}
+ false
+
+
+
+
+
+";
+
+ public string GetTempSecretProject(out string userSecretsId)
+ {
+ userSecretsId = Guid.NewGuid().ToString();
+ return CreateProject(userSecretsId);
+ }
+
+ public string CreateProject(string userSecretsId)
+ {
+ var projectPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "usersecretstest", Guid.NewGuid().ToString()));
+ var prop = string.IsNullOrEmpty(userSecretsId)
+ ? string.Empty
+ : $"{userSecretsId}";
+
+ File.WriteAllText(
+ Path.Combine(projectPath.FullName, "TestProject.csproj"),
+ string.Format(ProjectTemplate, prop));
+
+ var id = userSecretsId;
+ _disposables.Push(() =>
+ {
+ try
+ {
+ // may throw if id is bad
+ var secretsDir = Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(id));
+ TryDelete(secretsDir);
+ }
+ catch { }
+ });
+ _disposables.Push(() => TryDelete(projectPath.FullName));
+
+ return projectPath.FullName;
+ }
+
+ private static void TryDelete(string directory)
+ {
+ try
+ {
+ if (Directory.Exists(directory))
+ {
+ Directory.Delete(directory, true);
+ }
+ }
+ catch (Exception)
+ {
+ // Ignore failures.
+ Console.WriteLine("Failed to delete " + directory);
+ }
+ }
+ }
+}
diff --git a/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj b/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj
new file mode 100644
index 0000000000..0254a866e8
--- /dev/null
+++ b/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj
@@ -0,0 +1,21 @@
+
+
+
+ netcoreapp2.1
+ Microsoft.Extensions.SecretManager.Tools.Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Tools/dotnet-watch/README.md b/src/Tools/dotnet-watch/README.md
new file mode 100644
index 0000000000..ff7102a92e
--- /dev/null
+++ b/src/Tools/dotnet-watch/README.md
@@ -0,0 +1,88 @@
+dotnet-watch
+============
+`dotnet-watch` is a file watcher for `dotnet` that restarts the specified application when changes in the source code are detected.
+
+### How To Use
+
+The command must be executed in the directory that contains the project to be watched.
+
+ Usage: dotnet watch [options] [[--] ...]
+
+ Options:
+ -?|-h|--help Show help information
+ -q|--quiet Suppresses all output except warnings and errors
+ -v|--verbose Show verbose output
+
+Add `watch` after `dotnet` and before the command arguments that you want to run:
+
+| What you want to run | Dotnet watch command |
+| ---------------------------------------------- | -------------------------------------------------------- |
+| dotnet run | dotnet **watch** run |
+| dotnet run --arg1 value1 | dotnet **watch** run --arg1 value |
+| dotnet run --framework net451 -- --arg1 value1 | dotnet **watch** run --framework net451 -- --arg1 value1 |
+| dotnet test | dotnet **watch** test |
+
+### Environment variables
+
+Some configuration options can be passed to `dotnet watch` through environment variables. The available variables are:
+
+| Variable | Effect |
+| ---------------------------------------------- | -------------------------------------------------------- |
+| DOTNET_USE_POLLING_FILE_WATCHER | If set to "1" or "true", `dotnet watch` will use a polling file watcher instead of CoreFx's `FileSystemWatcher`. Used when watching files on network shares or Docker mounted volumes. |
+
+### MSBuild
+
+dotnet-watch can be configured from the MSBuild project file being watched.
+
+**Watch items**
+
+dotnet-watch will watch all items in the **Watch** item group.
+By default, this group inclues all items in **Compile** and **EmbeddedResource**.
+
+More items can be added to watch in a project file by adding items to 'Watch'.
+
+```xml
+
+
+
+
+```
+
+dotnet-watch will ignore Compile and EmbeddedResource items with the `Watch="false"` attribute.
+
+Example:
+
+```xml
+
+
+
+
+
+
+```
+
+**Project References**
+
+By default, dotnet-watch will scan the entire graph of project references and watch all files within those projects.
+
+dotnet-watch will ignore project references with the `Watch="false"` attribute.
+
+```xml
+
+
+
+```
+
+
+**Advanced configuration**
+
+dotnet-watch performs a design-time build to find items to watch.
+When this build is run, dotnet-watch will set the property `DotNetWatchBuild=true`.
+
+Example:
+
+```xml
+
+
+
+```
diff --git a/src/Tools/dotnet-watch/src/CommandLineOptions.cs b/src/Tools/dotnet-watch/src/CommandLineOptions.cs
new file mode 100644
index 0000000000..e6e23890d5
--- /dev/null
+++ b/src/Tools/dotnet-watch/src/CommandLineOptions.cs
@@ -0,0 +1,117 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using Microsoft.DotNet.Watcher.Tools;
+using Microsoft.Extensions.CommandLineUtils;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.DotNet.Watcher
+{
+ internal class CommandLineOptions
+ {
+ public string Project { get; private set; }
+ public bool IsHelp { get; private set; }
+ public bool IsQuiet { get; private set; }
+ public bool IsVerbose { get; private set; }
+ public IList RemainingArguments { get; private set; }
+ public bool ListFiles { get; private set; }
+
+ public static bool IsPollingEnabled
+ {
+ get
+ {
+ var envVar = Environment.GetEnvironmentVariable("DOTNET_USE_POLLING_FILE_WATCHER");
+ return envVar != null &&
+ (envVar.Equals("1", StringComparison.OrdinalIgnoreCase) ||
+ envVar.Equals("true", StringComparison.OrdinalIgnoreCase));
+ }
+ }
+
+ public static CommandLineOptions Parse(string[] args, IConsole console)
+ {
+ Ensure.NotNull(args, nameof(args));
+ Ensure.NotNull(console, nameof(console));
+
+ var app = new CommandLineApplication(throwOnUnexpectedArg: false)
+ {
+ Name = "dotnet watch",
+ FullName = "Microsoft DotNet File Watcher",
+ Out = console.Out,
+ Error = console.Error,
+ AllowArgumentSeparator = true,
+ ExtendedHelpText = @"
+Environment variables:
+
+ DOTNET_USE_POLLING_FILE_WATCHER
+ When set to '1' or 'true', dotnet-watch will poll the file system for
+ changes. This is required for some file systems, such as network shares,
+ Docker mounted volumes, and other virtual file systems.
+
+ DOTNET_WATCH
+ dotnet-watch sets this variable to '1' on all child processes launched.
+
+Remarks:
+ The special option '--' is used to delimit the end of the options and
+ the beginning of arguments that will be passed to the child dotnet process.
+ Its use is optional. When the special option '--' is not used,
+ dotnet-watch will use the first unrecognized argument as the beginning
+ of all arguments passed into the child dotnet process.
+
+ For example: dotnet watch -- --verbose run
+
+ Even though '--verbose' is an option dotnet-watch supports, the use of '--'
+ indicates that '--verbose' should be treated instead as an argument for
+ dotnet-run.
+
+Examples:
+ dotnet watch run
+ dotnet watch test
+"
+ };
+
+ app.HelpOption("-?|-h|--help");
+ // TODO multiple shouldn't be too hard to support
+ var optProjects = app.Option("-p|--project ", "The project to watch",
+ CommandOptionType.SingleValue);
+
+ var optQuiet = app.Option("-q|--quiet", "Suppresses all output except warnings and errors",
+ CommandOptionType.NoValue);
+ var optVerbose = app.VerboseOption();
+
+ var optList = app.Option("--list", "Lists all discovered files without starting the watcher",
+ CommandOptionType.NoValue);
+
+ app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly);
+
+ if (app.Execute(args) != 0)
+ {
+ return null;
+ }
+
+ if (optQuiet.HasValue() && optVerbose.HasValue())
+ {
+ throw new CommandParsingException(app, Resources.Error_QuietAndVerboseSpecified);
+ }
+
+ if (app.RemainingArguments.Count == 0
+ && !app.IsShowingInformation
+ && !optList.HasValue())
+ {
+ app.ShowHelp();
+ }
+
+ return new CommandLineOptions
+ {
+ Project = optProjects.Value(),
+ IsQuiet = optQuiet.HasValue(),
+ IsVerbose = optVerbose.HasValue(),
+ RemainingArguments = app.RemainingArguments,
+ IsHelp = app.IsShowingInformation,
+ ListFiles = optList.HasValue(),
+ };
+ }
+ }
+}
diff --git a/src/Tools/dotnet-watch/src/DotNetWatcher.cs b/src/Tools/dotnet-watch/src/DotNetWatcher.cs
new file mode 100644
index 0000000000..258ba0ab1e
--- /dev/null
+++ b/src/Tools/dotnet-watch/src/DotNetWatcher.cs
@@ -0,0 +1,102 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.DotNet.Watcher.Internal;
+using Microsoft.Extensions.CommandLineUtils;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.DotNet.Watcher
+{
+ public class DotNetWatcher
+ {
+ private readonly IReporter _reporter;
+ private readonly ProcessRunner _processRunner;
+
+ public DotNetWatcher(IReporter reporter)
+ {
+ Ensure.NotNull(reporter, nameof(reporter));
+
+ _reporter = reporter;
+ _processRunner = new ProcessRunner(reporter);
+ }
+
+ public async Task WatchAsync(ProcessSpec processSpec, IFileSetFactory fileSetFactory,
+ CancellationToken cancellationToken)
+ {
+ Ensure.NotNull(processSpec, nameof(processSpec));
+
+ var cancelledTaskSource = new TaskCompletionSource