diff --git a/.gitignore b/.gitignore
index 35ee1b2537..60749a6d7d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,5 @@ project.lock.json
artifacts/
*.nuget.props
.DS_Store
-Thumbs.db
\ No newline at end of file
+Thumbs.db
+test/WebSites/CliExample/wwwroot/api-docs/v1/*.json
\ No newline at end of file
diff --git a/README.md b/README.md
index fbf492d252..9123813b8d 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,7 @@ Swashbuckle consists of three packages - a Swagger generator, middleware to expo
|__Swashbuckle.AspNetCore.Swagger__|Exposes _SwaggerDocument_ objects as a JSON API. It expects an implementation of _ISwaggerProvider_ to be registered which it queries to retrieve Swagger document(s) before returning as serialized JSON|
|__Swashbuckle.AspNetCore.SwaggerGen__|Injects an implementation of _ISwaggerProvider_ that can be used by the above component. This particular implementation automatically generates _SwaggerDocument_(s) from your routes, controllers and models|
|__Swashbuckle.AspNetCore.SwaggerUI__|Exposes an embedded version of the swagger-ui. You specify the API endpoints where it can obtain Swagger JSON and it uses them to power interactive docs for your API|
+|__dotnet-swagger__ (Beta)|Provides a CLI interface for retrieving Swagger directly from a startup assembly, and writing to file|
# Configuration & Customization #
@@ -137,6 +138,9 @@ The steps described above will get you up and running with minimal setup. Howeve
* [Inject Custom CSS](#inject-custom-css)
* [Enable OAuth2.0 Flows](#enable-oauth20-flows)
+* [dotnet-swagger (CLI tool)](#dotnet-swagger-cli-tool)
+ * [Retrieve Swagger Directly from a Startup Assembly](#retrieve-swagger-directly-from-a-startup-assembly)
+
## Swashbuckle.AspNetCore.Swagger ##
### Change the Path for Swagger JSON Endpoints ###
@@ -780,3 +784,38 @@ app.UseSwaggerUI(c =>
c.ConfigureOAuth2("swagger-ui", "swagger-ui-secret", "swagger-ui-realm", "Swagger UI");
}
```
+
+## dotnet-swagger (CLI Tool) ##
+
+_NOTE:_ This feature is currently beta only. If you use it, please post feedback to the following [issue](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/541)
+
+### Retrieve Swagger Directly from a Startup Assembly ###
+
+The dotnet-swagger CLI tool can retrieve Swagger JSON directly from your application startup assembly, and write it to file. This can be useful if you want to incorporate Swagger generation into a CI/CD process, or if you want to serve it from static file at run-time.
+
+The tool can be installed as a [per-project, framework-dependent CLI extension](https://docs.microsoft.com/en-us/dotnet/core/tools/extensibility#per-project-based-extensibility) by adding the following reference to your .csproj file and running `dotnet restore`:
+
+```xml
+
+
+
+```
+
+Once this is done, you can run the following command from your project root:
+
+```
+dotnet swagger tofile --help
+```
+
+Before you invoke the `tofile` command, you need to ensure your application is configured to expose Swagger JSON, as described in [Getting Started](#getting-started). Once this is done, you can point to your startup assembly and generate a local Swagger JSON file with the following command:
+
+```
+dotnet swagger tofile [startupassembly] [swaggerdoc] [output]
+```
+
+Where ...
+* [startupassembly] is the relative path to your application's startup assembly
+* [swaggerdoc] is the name of the swagger document you want to retrieve, as configured in your startup class
+* [output] is the relative path where the Swagger JSON will be output to
+
+Checkout the [CliExample app](test/WebSites/CliExample) for more inspiration. It leverages the MSBuild Exec command to generate Swagger JSON at build-time.
\ No newline at end of file
diff --git a/Swashbuckle.AspNetCore.sln b/Swashbuckle.AspNetCore.sln
index d201ce40a4..5747f2f2e2 100644
--- a/Swashbuckle.AspNetCore.sln
+++ b/Swashbuckle.AspNetCore.sln
@@ -6,6 +6,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{15A55F4A-FC3
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FA1B4021-0A97-4F68-B966-148191F6AAA8}"
ProjectSection(SolutionItems) = preProject
+ .gitignore = .gitignore
appveyor.yml = appveyor.yml
CONTRIBUTING.md = CONTRIBUTING.md
ISSUE_TEMPLATE.md = ISSUE_TEMPLATE.md
@@ -57,6 +58,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Swashbuckle.AspNetCore.ReDo
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReDoc", "test\WebSites\ReDoc\ReDoc.csproj", "{311943AF-B796-42E7-B692-8D668B5FED77}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-swagger", "src\dotnet-swagger\dotnet-swagger.csproj", "{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-swagger.Test", "test\dotnet-swagger.Test\dotnet-swagger.Test.csproj", "{6570BBFF-55B9-4F76-8805-A9C89F284103}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliExample", "test\WebSites\CliExample\CliExample.csproj", "{1BFC563B-7C56-4797-B94E-CCD4096C94A5}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -259,6 +266,42 @@ Global
{311943AF-B796-42E7-B692-8D668B5FED77}.Release|x64.Build.0 = Release|Any CPU
{311943AF-B796-42E7-B692-8D668B5FED77}.Release|x86.ActiveCfg = Release|Any CPU
{311943AF-B796-42E7-B692-8D668B5FED77}.Release|x86.Build.0 = Release|Any CPU
+ {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|x64.Build.0 = Debug|Any CPU
+ {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|x86.Build.0 = Debug|Any CPU
+ {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|x64.ActiveCfg = Release|Any CPU
+ {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|x64.Build.0 = Release|Any CPU
+ {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|x86.ActiveCfg = Release|Any CPU
+ {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|x86.Build.0 = Release|Any CPU
+ {6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|x64.Build.0 = Debug|Any CPU
+ {6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|x86.Build.0 = Debug|Any CPU
+ {6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|x64.ActiveCfg = Release|Any CPU
+ {6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|x64.Build.0 = Release|Any CPU
+ {6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|x86.ActiveCfg = Release|Any CPU
+ {6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|x86.Build.0 = Release|Any CPU
+ {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|x64.Build.0 = Debug|Any CPU
+ {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|x86.Build.0 = Debug|Any CPU
+ {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|x64.ActiveCfg = Release|Any CPU
+ {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|x64.Build.0 = Release|Any CPU
+ {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|x86.ActiveCfg = Release|Any CPU
+ {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -281,6 +324,9 @@ Global
{A2345EE6-EDF0-4E79-8A83-C8EDF0AADB39} = {245144DE-BC89-4822-B044-020458BFECC0}
{B5E94C7D-B76E-4181-87E5-ACD8971A987E} = {15A55F4A-FC33-4D96-BAAD-FBDCDD96D5F5}
{311943AF-B796-42E7-B692-8D668B5FED77} = {245144DE-BC89-4822-B044-020458BFECC0}
+ {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4} = {15A55F4A-FC33-4D96-BAAD-FBDCDD96D5F5}
+ {6570BBFF-55B9-4F76-8805-A9C89F284103} = {1669F896-133C-4996-B58C-E7CDA299ADFF}
+ {1BFC563B-7C56-4797-B94E-CCD4096C94A5} = {245144DE-BC89-4822-B044-020458BFECC0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {36FC6A67-247D-4149-8EDD-79FFD1A75F51}
diff --git a/src/Swashbuckle.AspNetCore.Swagger/Application/SwaggerSerializerFactory.cs b/src/Swashbuckle.AspNetCore.Swagger/Application/SwaggerSerializerFactory.cs
index ffd41b8078..6d9fa2b459 100644
--- a/src/Swashbuckle.AspNetCore.Swagger/Application/SwaggerSerializerFactory.cs
+++ b/src/Swashbuckle.AspNetCore.Swagger/Application/SwaggerSerializerFactory.cs
@@ -6,7 +6,7 @@ namespace Swashbuckle.AspNetCore.Swagger
{
public class SwaggerSerializerFactory
{
- internal static JsonSerializer Create(IOptions applicationJsonOptions)
+ public static JsonSerializer Create(IOptions applicationJsonOptions)
{
// TODO: Should this handle case where mvcJsonOptions.Value == null?
return new JsonSerializer
diff --git a/src/dotnet-swagger/CommandRunner.cs b/src/dotnet-swagger/CommandRunner.cs
new file mode 100644
index 0000000000..2710bf08fc
--- /dev/null
+++ b/src/dotnet-swagger/CommandRunner.cs
@@ -0,0 +1,140 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Swashbuckle.AspNetCore.Cli
+{
+ public class CommandRunner
+ {
+ private readonly Dictionary _argumentDescriptors;
+ private readonly Dictionary _optionDescriptors;
+ private Func, int> _runFunc;
+ private readonly List _subRunners;
+ private readonly TextWriter _output;
+
+ public CommandRunner(string commandName, string commandDescription, TextWriter output)
+ {
+ CommandName = commandName;
+ CommandDescription = commandDescription;
+ _argumentDescriptors = new Dictionary();
+ _optionDescriptors = new Dictionary();
+ _runFunc = (namedArgs) => { return 1; }; // noop
+ _subRunners = new List();
+ _output = output;
+ }
+
+ public string CommandName { get; private set; }
+
+ public string CommandDescription { get; private set; }
+
+ public void Argument(string name, string description)
+ {
+ _argumentDescriptors.Add(name, description);
+ }
+
+ public void Option(string name, string description)
+ {
+ if (!name.StartsWith("--")) throw new ArgumentException("name of option must begin with --");
+ _optionDescriptors.Add(name, description);
+ }
+
+ public void OnRun(Func, int> runFunc)
+ {
+ _runFunc = runFunc;
+ }
+
+ public void SubCommand(string name, string description, Action configAction)
+ {
+ var runner = new CommandRunner($"{CommandName} {name}", description, _output);
+ configAction(runner);
+ _subRunners.Add(runner);
+ }
+
+ public int Run(IEnumerable args)
+ {
+ if (args.Any())
+ {
+ var subRunner = _subRunners.FirstOrDefault(r => r.CommandName.Split(' ').Last() == args.First());
+ if (subRunner != null) return subRunner.Run(args.Skip(1));
+ }
+
+ if (_subRunners.Any() || !TryParseArgs(args, out IDictionary namedArgs))
+ {
+ PrintUsage();
+ return 1;
+ }
+
+ return _runFunc(namedArgs);
+ }
+
+ private bool TryParseArgs(IEnumerable args, out IDictionary namedArgs)
+ {
+ namedArgs = new Dictionary();
+ var argsQueue = new Queue(args);
+
+ // Process options first
+ while (argsQueue.Any() && argsQueue.Peek().StartsWith("--"))
+ {
+ // Ensure it's expected and that the value is also provided
+ var name = argsQueue.Dequeue();
+ if (!_optionDescriptors.ContainsKey(name) || !argsQueue.Any() || argsQueue.Peek().StartsWith("--"))
+ return false;
+ namedArgs.Add(name, argsQueue.Dequeue());
+ }
+
+ // Process required args - ensure corresponding values are provided
+ foreach (var name in _argumentDescriptors.Keys)
+ {
+ if (!argsQueue.Any() || argsQueue.Peek().StartsWith("--")) return false;
+ namedArgs.Add(name, argsQueue.Dequeue());
+ }
+
+ return argsQueue.Count() == 0;
+ }
+
+ private void PrintUsage()
+ {
+ if (_subRunners.Any())
+ {
+ // List sub commands
+ _output.WriteLine(CommandDescription);
+ _output.WriteLine("Commands:");
+ foreach (var runner in _subRunners)
+ {
+ var shortName = runner.CommandName.Split(' ').Last();
+ if (shortName.StartsWith("_")) continue; // convention to hide commands
+ _output.WriteLine($" {shortName}: {runner.CommandDescription}");
+ }
+ _output.WriteLine();
+ }
+ else
+ {
+ // Usage for this command
+ var optionsPart = _optionDescriptors.Any() ? "[options] " : "";
+ var argParts = _argumentDescriptors.Keys.Select(name => $"[{name}]");
+ _output.WriteLine($"Usage: {CommandName} {optionsPart}{string.Join(" ", argParts)}");
+ _output.WriteLine();
+
+ // Arguments
+ foreach (var entry in _argumentDescriptors)
+ {
+ _output.WriteLine($"{entry.Key}:");
+ _output.WriteLine($" {entry.Value}");
+ _output.WriteLine();
+ }
+
+ // Options
+ if (_optionDescriptors.Any())
+ {
+ _output.WriteLine("options:");
+ foreach (var entry in _optionDescriptors)
+ {
+ _output.WriteLine($" {entry.Key}: {entry.Value}");
+ }
+ _output.WriteLine();
+ }
+ }
+ }
+ }
+}
diff --git a/src/dotnet-swagger/Program.cs b/src/dotnet-swagger/Program.cs
new file mode 100644
index 0000000000..ef8750c002
--- /dev/null
+++ b/src/dotnet-swagger/Program.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Reflection;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.Loader;
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Options;
+using Microsoft.AspNetCore.Mvc;
+using Swashbuckle.AspNetCore.Swagger;
+
+namespace Swashbuckle.AspNetCore.Cli
+{
+ class Program
+ {
+ static int Main(string[] args)
+ {
+ // Helper to simplify command line parsing etc.
+ var runner = new CommandRunner("dotnet swagger", "Swashbuckle (Swagger) Command Line Tools", Console.Out);
+
+ // NOTE: The "dotnet swagger tofile" command does not serve the request directly. Instead, it invokes a corresponding
+ // command (called _tofile) via "dotnet exec" so that the runtime configuration (*.runtimeconfig & *.deps.json) of the
+ // provided startupassembly can be used instead of the tool's. This is neccessary to successfully load the
+ // startupassembly and it's transitive dependencies. See https://github.com/dotnet/coreclr/issues/13277 for more.
+
+ // > dotnet swagger tofile ...
+ runner.SubCommand("tofile", "retrieves Swagger from a startup assembly, and writes to file ", c =>
+ {
+ c.Argument("startupassembly", "relative path to the application's startup assembly");
+ c.Argument("swaggerdoc", "name of the swagger doc you want to retrieve, as configured in your startup class");
+ c.Argument("output", "relative path where the Swagger will be output");
+ c.Option("--host", "a specific host to include in the Swagger output");
+ c.Option("--basepath", "a specific basePath to inlcude in the Swagger output");
+ c.OnRun((namedArgs) =>
+ {
+ var depsFile = namedArgs["startupassembly"].Replace(".dll", ".deps.json");
+ var runtimeConfig = namedArgs["startupassembly"].Replace(".dll", ".runtimeconfig.json");
+
+ var subProcess = Process.Start("dotnet", string.Format(
+ "exec --depsfile {0} --runtimeconfig {1} {2} _{3}", // note the underscore
+ depsFile,
+ runtimeConfig,
+ typeof(Program).GetTypeInfo().Assembly.Location,
+ string.Join(" ", args)
+ ));
+
+ subProcess.WaitForExit();
+ return subProcess.ExitCode;
+ });
+ });
+
+ // > dotnet swagger _tofile ... (* should only be invoked via "dotnet exec")
+ runner.SubCommand("_tofile", "retrieves Swagger from a startup assembly, and writes to file ", c =>
+ {
+ c.Argument("startupassembly", "relative path to the application's startup assembly");
+ c.Argument("swaggerdoc", "name of the swagger doc you want to retrieve, as configured in your startup class");
+ c.Argument("output", "relative path where the Swagger will be output");
+ c.Option("--host", "a specific host to include in the Swagger output");
+ c.Option("--basepath", "a specific basePath to inlcude in the Swagger output");
+ c.OnRun((namedArgs) =>
+ {
+ // 1) Configure host with provided startupassembly
+ var startupAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(
+ $"{Directory.GetCurrentDirectory()}\\{namedArgs["startupassembly"]}");
+ var host = WebHost.CreateDefaultBuilder()
+ .UseStartup(startupAssembly.FullName)
+ .Build();
+
+ // 2) Retrieve Swagger via configured provider
+ var swaggerProvider = (ISwaggerProvider)host.Services.GetService(typeof(ISwaggerProvider));
+ var swagger = swaggerProvider.GetSwagger(
+ namedArgs["swaggerdoc"],
+ namedArgs.ContainsKey("--host") ? namedArgs["--host"] : null,
+ namedArgs.ContainsKey("--basepath") ? namedArgs["--basepath"] : null,
+ null);
+
+ // 3) Write to specified output location
+ var outputPath = $"{Directory.GetCurrentDirectory()}\\{namedArgs["output"]}";
+ var mvcOptionsAccessor = (IOptions)host.Services.GetService(typeof(IOptions));
+ var serializer = SwaggerSerializerFactory.Create(mvcOptionsAccessor);
+ using (var streamWriter = File.CreateText(outputPath))
+ {
+ serializer.Serialize(streamWriter, swagger);
+ Console.WriteLine($"Swagger JSON succesfully written to {outputPath}");
+ }
+
+ return 0;
+ });
+ });
+
+ return runner.Run(args);
+ }
+ }
+}
diff --git a/src/dotnet-swagger/dotnet-swagger.csproj b/src/dotnet-swagger/dotnet-swagger.csproj
new file mode 100644
index 0000000000..fcc1eede1b
--- /dev/null
+++ b/src/dotnet-swagger/dotnet-swagger.csproj
@@ -0,0 +1,18 @@
+
+
+
+ TODO
+ netcoreapp2.0
+ 1.2.0-beta1
+ Exe
+ Swashbuckle.AspNetCore.Cli
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/WebSites/CliExample/CliExample.csproj b/test/WebSites/CliExample/CliExample.csproj
new file mode 100644
index 0000000000..07934321c4
--- /dev/null
+++ b/test/WebSites/CliExample/CliExample.csproj
@@ -0,0 +1,29 @@
+
+
+
+ netcoreapp2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/WebSites/CliExample/Controllers/CrudActionsController.cs b/test/WebSites/CliExample/Controllers/CrudActionsController.cs
new file mode 100644
index 0000000000..14c5879b08
--- /dev/null
+++ b/test/WebSites/CliExample/Controllers/CrudActionsController.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CliExample.Controllers
+{
+ [Route("/products")]
+ [Produces("application/json")]
+ public class CrudActionsController
+ {
+ [HttpPost]
+ public int Create([FromBody, Required]Product product)
+ {
+ return 1;
+ }
+
+ [HttpGet]
+ public IEnumerable GetAll()
+ {
+ return new[]
+ {
+ new Product { Id = 1, Description = "A product" },
+ new Product { Id = 2, Description = "Another product" },
+ };
+ }
+
+ [HttpGet("{id}")]
+ public Product GetById(int id)
+ {
+ return new Product { Id = id, Description = "A product" };
+ }
+
+ [HttpPut("{id}")]
+ public void Update(int id, [FromBody, Required]Product product)
+ {
+ }
+
+ [HttpPatch("{id}")]
+ public void PartialUpdate(int id, [FromBody, Required]IDictionary updates)
+ {
+ }
+
+ [HttpDelete("{id}")]
+ public void Delete(int id)
+ {
+ }
+ }
+
+ public class Product
+ {
+ public int Id { get; set; }
+
+ public string Description { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/CliExample/Program.cs b/test/WebSites/CliExample/Program.cs
new file mode 100644
index 0000000000..77727f4872
--- /dev/null
+++ b/test/WebSites/CliExample/Program.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace CliExample
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ BuildWebHost(args).Run();
+ }
+
+ public static IWebHost BuildWebHost(string[] args) =>
+ WebHost.CreateDefaultBuilder(args)
+ .UseStartup()
+ .Build();
+ }
+}
diff --git a/test/WebSites/CliExample/Properties/launchSettings.json b/test/WebSites/CliExample/Properties/launchSettings.json
new file mode 100644
index 0000000000..589e4e8aec
--- /dev/null
+++ b/test/WebSites/CliExample/Properties/launchSettings.json
@@ -0,0 +1,28 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:51071/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "api-docs",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "CliExample": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "http://localhost:51072/"
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/CliExample/Startup.cs b/test/WebSites/CliExample/Startup.cs
new file mode 100644
index 0000000000..1074c7832a
--- /dev/null
+++ b/test/WebSites/CliExample/Startup.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CliExample
+{
+ public class Startup
+ {
+ // This method gets called by the runtime. Use this method to add services to the container.
+ // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddSwaggerGen(c =>
+ {
+ c.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info { Title = "CRUD API", Version = "v1" });
+ });
+
+ services.AddMvc();
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IHostingEnvironment env)
+ {
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+
+ // If available, serve Swagger JSON from static file (i.e. pre-generated via "dotnet swagger")
+ app.UseStaticFiles();
+
+ // Otherwise, generate on-the-fly via regular Swagger middleware
+ app.UseSwagger(c =>
+ {
+ c.RouteTemplate = "api-docs/{documentName}/swagger.json";
+ c.PreSerializeFilters.Add((swagger, httpReq) => swagger.Host = httpReq.Host.Value);
+ });
+
+ app.UseSwaggerUI(c =>
+ {
+ c.RoutePrefix = "api-docs";
+ c.SwaggerEndpoint("v1/swagger.json", "V1 Docs");
+ });
+
+ app.UseMvc();
+ }
+ }
+}
diff --git a/test/WebSites/CliExample/wwwroot/api-docs/v1/.gitkeep b/test/WebSites/CliExample/wwwroot/api-docs/v1/.gitkeep
new file mode 100644
index 0000000000..5f282702bb
--- /dev/null
+++ b/test/WebSites/CliExample/wwwroot/api-docs/v1/.gitkeep
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test/dotnet-swagger.Test/CommandRunnerTests.cs b/test/dotnet-swagger.Test/CommandRunnerTests.cs
new file mode 100644
index 0000000000..8ad056c34c
--- /dev/null
+++ b/test/dotnet-swagger.Test/CommandRunnerTests.cs
@@ -0,0 +1,106 @@
+using System.Collections.Generic;
+using System.IO;
+using Xunit;
+
+namespace Swashbuckle.AspNetCore.Cli.Test
+{
+ public class CommandRunnerTests
+ {
+ [Fact]
+ public void Run_ParsesArgumentsAndExecutesCommands_AccordingToPreConfiguredMetadata()
+ {
+ var receivedValues = new List();
+ var subject = new CommandRunner("test", "a test", new StringWriter());
+ subject.SubCommand("cmd1", "", c => {
+ c.Option("--opt1", "");
+ c.Argument("arg1", "");
+ c.OnRun((namedArgs) =>
+ {
+ receivedValues.Add(namedArgs["--opt1"]);
+ receivedValues.Add(namedArgs["arg1"]);
+ return 2;
+ });
+ });
+ subject.SubCommand("cmd2", "", c => {
+ c.Option("--opt1", "");
+ c.Argument("arg1", "");
+ c.OnRun((namedArgs) =>
+ {
+ receivedValues.Add(namedArgs["--opt1"]);
+ receivedValues.Add(namedArgs["arg1"]);
+ return 3;
+ });
+ });
+
+ var cmd1ExitCode = subject.Run(new[] { "cmd1", "--opt1", "foo", "bar" });
+ var cmd2ExitCode = subject.Run(new[] { "cmd2", "--opt1", "blah", "dblah" });
+
+ Assert.Equal(2, cmd1ExitCode);
+ Assert.Equal(3, cmd2ExitCode);
+ Assert.Equal(new[] { "foo", "bar", "blah", "dblah" }, receivedValues.ToArray());
+ }
+
+ [Fact]
+ public void Run_PrintsAvailableCommands_WhenUnexpectedCommandIsProvided()
+ {
+ var output = new StringWriter();
+ var subject = new CommandRunner("test", "a test", output);
+ subject.SubCommand("cmd", "does something", c => {
+ });
+
+ var exitCode = subject.Run(new[] { "foo" });
+
+ Assert.StartsWith("a test", output.ToString());
+ Assert.Contains("Commands:", output.ToString());
+ Assert.Contains("cmd: does something", output.ToString());
+ }
+
+ [Fact]
+ public void Run_PrintsAvailableCommands_WhenHelpOptionIsProvided()
+ {
+ var output = new StringWriter();
+ var subject = new CommandRunner("test", "a test", output);
+ subject.SubCommand("cmd", "does something", c => {
+ });
+
+ var exitCode = subject.Run(new[] { "--help" });
+
+ Assert.StartsWith("a test", output.ToString());
+ Assert.Contains("Commands:", output.ToString());
+ Assert.Contains("cmd: does something", output.ToString());
+ }
+
+ [Theory]
+ [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt2", "foo" }, true)]
+ [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt1" }, true)]
+ [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt1", "--opt2" }, true)]
+ [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt1", "foo" }, false)]
+ [InlineData(new string[] { }, new[] { "arg1" }, new[] { "cmd" }, true)]
+ [InlineData(new string[] { }, new[] { "arg1" }, new[] { "cmd", "--opt1" }, true)]
+ [InlineData(new string[] {}, new[] { "arg1" }, new[] { "cmd", "foo", "bar" }, true)]
+ [InlineData(new string[] {}, new[] { "arg1" }, new[] { "cmd", "foo" }, false)]
+ public void Run_PrintsCommandUsage_WhenUnexpectedArgumentsAreProvided(
+ string[] optionNames,
+ string[] argNames,
+ string[] providedArgs,
+ bool shouldPrintUsage)
+ {
+ var output = new StringWriter();
+ var subject = new CommandRunner("test", "a test", output);
+ subject.SubCommand("cmd", "a command", c =>
+ {
+ foreach (var name in optionNames)
+ c.Option(name, "");
+ foreach (var name in argNames)
+ c.Argument(name, "");
+ });
+
+ subject.Run(providedArgs);
+
+ if (shouldPrintUsage)
+ Assert.StartsWith("Usage: test cmd", output.ToString());
+ else
+ Assert.Empty(output.ToString());
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/dotnet-swagger.Test/dotnet-swagger.Test.csproj b/test/dotnet-swagger.Test/dotnet-swagger.Test.csproj
new file mode 100644
index 0000000000..9c59ded81b
--- /dev/null
+++ b/test/dotnet-swagger.Test/dotnet-swagger.Test.csproj
@@ -0,0 +1,22 @@
+
+
+
+ netcoreapp2.0
+
+ false
+
+ Swashbuckle.AspNetCore.Cli
+
+
+
+
+
+
+
+
+
+
+
+
+
+