Skip to content

Commit 26a56fe

Browse files
Merge pull request #590 from domaindrivendev/cli-tools
Add dotnet-swagger CLI tool beta
2 parents c8ae98e + c121b93 commit 26a56fe

File tree

15 files changed

+659
-2
lines changed

15 files changed

+659
-2
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ project.lock.json
1010
artifacts/
1111
*.nuget.props
1212
.DS_Store
13-
Thumbs.db
13+
Thumbs.db
14+
test/WebSites/CliExample/wwwroot/api-docs/v1/*.json

README.md

+39
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Swashbuckle consists of three packages - a Swagger generator, middleware to expo
103103
|__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|
104104
|__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|
105105
|__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|
106+
|__dotnet-swagger__ (Beta)|Provides a CLI interface for retrieving Swagger directly from a startup assembly, and writing to file|
106107

107108
# Configuration & Customization #
108109

@@ -137,6 +138,9 @@ The steps described above will get you up and running with minimal setup. Howeve
137138
* [Inject Custom CSS](#inject-custom-css)
138139
* [Enable OAuth2.0 Flows](#enable-oauth20-flows)
139140

141+
* [dotnet-swagger (CLI tool)](#dotnet-swagger-cli-tool)
142+
* [Retrieve Swagger Directly from a Startup Assembly](#retrieve-swagger-directly-from-a-startup-assembly)
143+
140144
## Swashbuckle.AspNetCore.Swagger ##
141145

142146
### Change the Path for Swagger JSON Endpoints ###
@@ -780,3 +784,38 @@ app.UseSwaggerUI(c =>
780784
c.ConfigureOAuth2("swagger-ui", "swagger-ui-secret", "swagger-ui-realm", "Swagger UI");
781785
}
782786
```
787+
788+
## dotnet-swagger (CLI Tool) ##
789+
790+
_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)
791+
792+
### Retrieve Swagger Directly from a Startup Assembly ###
793+
794+
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.
795+
796+
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`:
797+
798+
```xml
799+
<ItemGroup>
800+
<DotNetCliToolReference Include="dotnet-swagger" Version="1.2.0-beta1" />
801+
</ItemGroup>
802+
```
803+
804+
Once this is done, you can run the following command from your project root:
805+
806+
```
807+
dotnet swagger tofile --help
808+
```
809+
810+
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:
811+
812+
```
813+
dotnet swagger tofile [startupassembly] [swaggerdoc] [output]
814+
```
815+
816+
Where ...
817+
* [startupassembly] is the relative path to your application's startup assembly
818+
* [swaggerdoc] is the name of the swagger document you want to retrieve, as configured in your startup class
819+
* [output] is the relative path where the Swagger JSON will be output to
820+
821+
Checkout the [CliExample app](test/WebSites/CliExample) for more inspiration. It leverages the MSBuild Exec command to generate Swagger JSON at build-time.

Swashbuckle.AspNetCore.sln

+46
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{15A55F4A-FC3
66
EndProject
77
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FA1B4021-0A97-4F68-B966-148191F6AAA8}"
88
ProjectSection(SolutionItems) = preProject
9+
.gitignore = .gitignore
910
appveyor.yml = appveyor.yml
1011
CONTRIBUTING.md = CONTRIBUTING.md
1112
ISSUE_TEMPLATE.md = ISSUE_TEMPLATE.md
@@ -57,6 +58,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Swashbuckle.AspNetCore.ReDo
5758
EndProject
5859
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReDoc", "test\WebSites\ReDoc\ReDoc.csproj", "{311943AF-B796-42E7-B692-8D668B5FED77}"
5960
EndProject
61+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-swagger", "src\dotnet-swagger\dotnet-swagger.csproj", "{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}"
62+
EndProject
63+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-swagger.Test", "test\dotnet-swagger.Test\dotnet-swagger.Test.csproj", "{6570BBFF-55B9-4F76-8805-A9C89F284103}"
64+
EndProject
65+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliExample", "test\WebSites\CliExample\CliExample.csproj", "{1BFC563B-7C56-4797-B94E-CCD4096C94A5}"
66+
EndProject
6067
Global
6168
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6269
Debug|Any CPU = Debug|Any CPU
@@ -259,6 +266,42 @@ Global
259266
{311943AF-B796-42E7-B692-8D668B5FED77}.Release|x64.Build.0 = Release|Any CPU
260267
{311943AF-B796-42E7-B692-8D668B5FED77}.Release|x86.ActiveCfg = Release|Any CPU
261268
{311943AF-B796-42E7-B692-8D668B5FED77}.Release|x86.Build.0 = Release|Any CPU
269+
{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
270+
{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
271+
{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|x64.ActiveCfg = Debug|Any CPU
272+
{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|x64.Build.0 = Debug|Any CPU
273+
{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|x86.ActiveCfg = Debug|Any CPU
274+
{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|x86.Build.0 = Debug|Any CPU
275+
{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
276+
{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|Any CPU.Build.0 = Release|Any CPU
277+
{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|x64.ActiveCfg = Release|Any CPU
278+
{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|x64.Build.0 = Release|Any CPU
279+
{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|x86.ActiveCfg = Release|Any CPU
280+
{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|x86.Build.0 = Release|Any CPU
281+
{6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
282+
{6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|Any CPU.Build.0 = Debug|Any CPU
283+
{6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|x64.ActiveCfg = Debug|Any CPU
284+
{6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|x64.Build.0 = Debug|Any CPU
285+
{6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|x86.ActiveCfg = Debug|Any CPU
286+
{6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|x86.Build.0 = Debug|Any CPU
287+
{6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|Any CPU.ActiveCfg = Release|Any CPU
288+
{6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|Any CPU.Build.0 = Release|Any CPU
289+
{6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|x64.ActiveCfg = Release|Any CPU
290+
{6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|x64.Build.0 = Release|Any CPU
291+
{6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|x86.ActiveCfg = Release|Any CPU
292+
{6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|x86.Build.0 = Release|Any CPU
293+
{1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
294+
{1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
295+
{1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|x64.ActiveCfg = Debug|Any CPU
296+
{1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|x64.Build.0 = Debug|Any CPU
297+
{1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|x86.ActiveCfg = Debug|Any CPU
298+
{1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|x86.Build.0 = Debug|Any CPU
299+
{1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
300+
{1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|Any CPU.Build.0 = Release|Any CPU
301+
{1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|x64.ActiveCfg = Release|Any CPU
302+
{1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|x64.Build.0 = Release|Any CPU
303+
{1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|x86.ActiveCfg = Release|Any CPU
304+
{1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|x86.Build.0 = Release|Any CPU
262305
EndGlobalSection
263306
GlobalSection(SolutionProperties) = preSolution
264307
HideSolutionNode = FALSE
@@ -281,6 +324,9 @@ Global
281324
{A2345EE6-EDF0-4E79-8A83-C8EDF0AADB39} = {245144DE-BC89-4822-B044-020458BFECC0}
282325
{B5E94C7D-B76E-4181-87E5-ACD8971A987E} = {15A55F4A-FC33-4D96-BAAD-FBDCDD96D5F5}
283326
{311943AF-B796-42E7-B692-8D668B5FED77} = {245144DE-BC89-4822-B044-020458BFECC0}
327+
{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4} = {15A55F4A-FC33-4D96-BAAD-FBDCDD96D5F5}
328+
{6570BBFF-55B9-4F76-8805-A9C89F284103} = {1669F896-133C-4996-B58C-E7CDA299ADFF}
329+
{1BFC563B-7C56-4797-B94E-CCD4096C94A5} = {245144DE-BC89-4822-B044-020458BFECC0}
284330
EndGlobalSection
285331
GlobalSection(ExtensibilityGlobals) = postSolution
286332
SolutionGuid = {36FC6A67-247D-4149-8EDD-79FFD1A75F51}

src/Swashbuckle.AspNetCore.Swagger/Application/SwaggerSerializerFactory.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace Swashbuckle.AspNetCore.Swagger
66
{
77
public class SwaggerSerializerFactory
88
{
9-
internal static JsonSerializer Create(IOptions<MvcJsonOptions> applicationJsonOptions)
9+
public static JsonSerializer Create(IOptions<MvcJsonOptions> applicationJsonOptions)
1010
{
1111
// TODO: Should this handle case where mvcJsonOptions.Value == null?
1212
return new JsonSerializer

src/dotnet-swagger/CommandRunner.cs

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
6+
namespace Swashbuckle.AspNetCore.Cli
7+
{
8+
public class CommandRunner
9+
{
10+
private readonly Dictionary<string, string> _argumentDescriptors;
11+
private readonly Dictionary<string, string> _optionDescriptors;
12+
private Func<IDictionary<string, string>, int> _runFunc;
13+
private readonly List<CommandRunner> _subRunners;
14+
private readonly TextWriter _output;
15+
16+
public CommandRunner(string commandName, string commandDescription, TextWriter output)
17+
{
18+
CommandName = commandName;
19+
CommandDescription = commandDescription;
20+
_argumentDescriptors = new Dictionary<string, string>();
21+
_optionDescriptors = new Dictionary<string, string>();
22+
_runFunc = (namedArgs) => { return 1; }; // noop
23+
_subRunners = new List<CommandRunner>();
24+
_output = output;
25+
}
26+
27+
public string CommandName { get; private set; }
28+
29+
public string CommandDescription { get; private set; }
30+
31+
public void Argument(string name, string description)
32+
{
33+
_argumentDescriptors.Add(name, description);
34+
}
35+
36+
public void Option(string name, string description)
37+
{
38+
if (!name.StartsWith("--")) throw new ArgumentException("name of option must begin with --");
39+
_optionDescriptors.Add(name, description);
40+
}
41+
42+
public void OnRun(Func<IDictionary<string, string>, int> runFunc)
43+
{
44+
_runFunc = runFunc;
45+
}
46+
47+
public void SubCommand(string name, string description, Action<CommandRunner> configAction)
48+
{
49+
var runner = new CommandRunner($"{CommandName} {name}", description, _output);
50+
configAction(runner);
51+
_subRunners.Add(runner);
52+
}
53+
54+
public int Run(IEnumerable<string> args)
55+
{
56+
if (args.Any())
57+
{
58+
var subRunner = _subRunners.FirstOrDefault(r => r.CommandName.Split(' ').Last() == args.First());
59+
if (subRunner != null) return subRunner.Run(args.Skip(1));
60+
}
61+
62+
if (_subRunners.Any() || !TryParseArgs(args, out IDictionary<string, string> namedArgs))
63+
{
64+
PrintUsage();
65+
return 1;
66+
}
67+
68+
return _runFunc(namedArgs);
69+
}
70+
71+
private bool TryParseArgs(IEnumerable<string> args, out IDictionary<string, string> namedArgs)
72+
{
73+
namedArgs = new Dictionary<string, string>();
74+
var argsQueue = new Queue<string>(args);
75+
76+
// Process options first
77+
while (argsQueue.Any() && argsQueue.Peek().StartsWith("--"))
78+
{
79+
// Ensure it's expected and that the value is also provided
80+
var name = argsQueue.Dequeue();
81+
if (!_optionDescriptors.ContainsKey(name) || !argsQueue.Any() || argsQueue.Peek().StartsWith("--"))
82+
return false;
83+
namedArgs.Add(name, argsQueue.Dequeue());
84+
}
85+
86+
// Process required args - ensure corresponding values are provided
87+
foreach (var name in _argumentDescriptors.Keys)
88+
{
89+
if (!argsQueue.Any() || argsQueue.Peek().StartsWith("--")) return false;
90+
namedArgs.Add(name, argsQueue.Dequeue());
91+
}
92+
93+
return argsQueue.Count() == 0;
94+
}
95+
96+
private void PrintUsage()
97+
{
98+
if (_subRunners.Any())
99+
{
100+
// List sub commands
101+
_output.WriteLine(CommandDescription);
102+
_output.WriteLine("Commands:");
103+
foreach (var runner in _subRunners)
104+
{
105+
var shortName = runner.CommandName.Split(' ').Last();
106+
if (shortName.StartsWith("_")) continue; // convention to hide commands
107+
_output.WriteLine($" {shortName}: {runner.CommandDescription}");
108+
}
109+
_output.WriteLine();
110+
}
111+
else
112+
{
113+
// Usage for this command
114+
var optionsPart = _optionDescriptors.Any() ? "[options] " : "";
115+
var argParts = _argumentDescriptors.Keys.Select(name => $"[{name}]");
116+
_output.WriteLine($"Usage: {CommandName} {optionsPart}{string.Join(" ", argParts)}");
117+
_output.WriteLine();
118+
119+
// Arguments
120+
foreach (var entry in _argumentDescriptors)
121+
{
122+
_output.WriteLine($"{entry.Key}:");
123+
_output.WriteLine($" {entry.Value}");
124+
_output.WriteLine();
125+
}
126+
127+
// Options
128+
if (_optionDescriptors.Any())
129+
{
130+
_output.WriteLine("options:");
131+
foreach (var entry in _optionDescriptors)
132+
{
133+
_output.WriteLine($" {entry.Key}: {entry.Value}");
134+
}
135+
_output.WriteLine();
136+
}
137+
}
138+
}
139+
}
140+
}

src/dotnet-swagger/Program.cs

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System;
2+
using System.Reflection;
3+
using System.Diagnostics;
4+
using System.IO;
5+
using System.Runtime.Loader;
6+
using Microsoft.AspNetCore;
7+
using Microsoft.AspNetCore.Hosting;
8+
using Microsoft.Extensions.Options;
9+
using Microsoft.AspNetCore.Mvc;
10+
using Swashbuckle.AspNetCore.Swagger;
11+
12+
namespace Swashbuckle.AspNetCore.Cli
13+
{
14+
class Program
15+
{
16+
static int Main(string[] args)
17+
{
18+
// Helper to simplify command line parsing etc.
19+
var runner = new CommandRunner("dotnet swagger", "Swashbuckle (Swagger) Command Line Tools", Console.Out);
20+
21+
// NOTE: The "dotnet swagger tofile" command does not serve the request directly. Instead, it invokes a corresponding
22+
// command (called _tofile) via "dotnet exec" so that the runtime configuration (*.runtimeconfig & *.deps.json) of the
23+
// provided startupassembly can be used instead of the tool's. This is neccessary to successfully load the
24+
// startupassembly and it's transitive dependencies. See https://github.com/dotnet/coreclr/issues/13277 for more.
25+
26+
// > dotnet swagger tofile ...
27+
runner.SubCommand("tofile", "retrieves Swagger from a startup assembly, and writes to file ", c =>
28+
{
29+
c.Argument("startupassembly", "relative path to the application's startup assembly");
30+
c.Argument("swaggerdoc", "name of the swagger doc you want to retrieve, as configured in your startup class");
31+
c.Argument("output", "relative path where the Swagger will be output");
32+
c.Option("--host", "a specific host to include in the Swagger output");
33+
c.Option("--basepath", "a specific basePath to inlcude in the Swagger output");
34+
c.OnRun((namedArgs) =>
35+
{
36+
var depsFile = namedArgs["startupassembly"].Replace(".dll", ".deps.json");
37+
var runtimeConfig = namedArgs["startupassembly"].Replace(".dll", ".runtimeconfig.json");
38+
39+
var subProcess = Process.Start("dotnet", string.Format(
40+
"exec --depsfile {0} --runtimeconfig {1} {2} _{3}", // note the underscore
41+
depsFile,
42+
runtimeConfig,
43+
typeof(Program).GetTypeInfo().Assembly.Location,
44+
string.Join(" ", args)
45+
));
46+
47+
subProcess.WaitForExit();
48+
return subProcess.ExitCode;
49+
});
50+
});
51+
52+
// > dotnet swagger _tofile ... (* should only be invoked via "dotnet exec")
53+
runner.SubCommand("_tofile", "retrieves Swagger from a startup assembly, and writes to file ", c =>
54+
{
55+
c.Argument("startupassembly", "relative path to the application's startup assembly");
56+
c.Argument("swaggerdoc", "name of the swagger doc you want to retrieve, as configured in your startup class");
57+
c.Argument("output", "relative path where the Swagger will be output");
58+
c.Option("--host", "a specific host to include in the Swagger output");
59+
c.Option("--basepath", "a specific basePath to inlcude in the Swagger output");
60+
c.OnRun((namedArgs) =>
61+
{
62+
// 1) Configure host with provided startupassembly
63+
var startupAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(
64+
$"{Directory.GetCurrentDirectory()}\\{namedArgs["startupassembly"]}");
65+
var host = WebHost.CreateDefaultBuilder()
66+
.UseStartup(startupAssembly.FullName)
67+
.Build();
68+
69+
// 2) Retrieve Swagger via configured provider
70+
var swaggerProvider = (ISwaggerProvider)host.Services.GetService(typeof(ISwaggerProvider));
71+
var swagger = swaggerProvider.GetSwagger(
72+
namedArgs["swaggerdoc"],
73+
namedArgs.ContainsKey("--host") ? namedArgs["--host"] : null,
74+
namedArgs.ContainsKey("--basepath") ? namedArgs["--basepath"] : null,
75+
null);
76+
77+
// 3) Write to specified output location
78+
var outputPath = $"{Directory.GetCurrentDirectory()}\\{namedArgs["output"]}";
79+
var mvcOptionsAccessor = (IOptions<MvcJsonOptions>)host.Services.GetService(typeof(IOptions<MvcJsonOptions>));
80+
var serializer = SwaggerSerializerFactory.Create(mvcOptionsAccessor);
81+
using (var streamWriter = File.CreateText(outputPath))
82+
{
83+
serializer.Serialize(streamWriter, swagger);
84+
Console.WriteLine($"Swagger JSON succesfully written to {outputPath}");
85+
}
86+
87+
return 0;
88+
});
89+
});
90+
91+
return runner.Run(args);
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)