/
Program.cs
216 lines (187 loc) · 9.48 KB
/
Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Writers;
using Swashbuckle.AspNetCore.Swagger;
namespace Swashbuckle.AspNetCore.Cli
{
public class Program
{
public 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.Option("--output", "relative path where the Swagger will be output, defaults to stdout");
c.Option("--host", "a specific host to include in the Swagger output");
c.Option("--basepath", "a specific basePath to include in the Swagger output");
c.Option("--serializeasv2", "output Swagger in the V2 format rather than V3", true);
c.Option("--yaml", "exports swagger in a yaml format", true);
c.OnRun((namedArgs) =>
{
if (!File.Exists(namedArgs["startupassembly"]))
throw new FileNotFoundException(namedArgs["startupassembly"]);
var depsFile = namedArgs["startupassembly"].Replace(".dll", ".deps.json");
var runtimeConfig = namedArgs["startupassembly"].Replace(".dll", ".runtimeconfig.json");
var commandName = args[0];
var subProcessArguments = new string[args.Length - 1];
if (subProcessArguments.Length > 0)
{
Array.Copy(args, 1, subProcessArguments, 0, subProcessArguments.Length);
}
var subProcessCommandLine = string.Format(
"exec --depsfile {0} --runtimeconfig {1} {2} _{3} {4}", // note the underscore prepended to the command name
EscapePath(depsFile),
EscapePath(runtimeConfig),
EscapePath(typeof(Program).GetTypeInfo().Assembly.Location),
commandName,
string.Join(" ", subProcessArguments.Select(x => EscapePath(x)))
);
var subProcess = Process.Start("dotnet", subProcessCommandLine);
subProcess.WaitForExit();
return subProcess.ExitCode;
});
});
// > dotnet swagger _tofile ... (* should only be invoked via "dotnet exec")
runner.SubCommand("_tofile", "", c =>
{
c.Argument("startupassembly", "");
c.Argument("swaggerdoc", "");
c.Option("--output", "");
c.Option("--host", "");
c.Option("--basepath", "");
c.Option("--serializeasv2", "", true);
c.Option("--yaml", "", true);
c.OnRun((namedArgs) =>
{
// 1) Configure host with provided startupassembly
var startupAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(
Path.Combine(Directory.GetCurrentDirectory(), namedArgs["startupassembly"]));
// 2) Build a service container that's based on the startup assembly
var serviceProvider = GetServiceProvider(startupAssembly);
// 3) Retrieve Swagger via configured provider
var swaggerProvider = serviceProvider.GetRequiredService<ISwaggerProvider>();
var swaggerOptions = serviceProvider.GetService<IOptions<SwaggerOptions>>();
var swaggerDocumentSerializer = swaggerOptions?.Value?.CustomDocumentSerializer;
var swagger = swaggerProvider.GetSwagger(
namedArgs["swaggerdoc"],
namedArgs.TryGetValue("--host", out var arg) ? arg : null,
namedArgs.TryGetValue("--basepath", out var namedArg) ? namedArg : null);
// 4) Serialize to specified output location or stdout
var outputPath = namedArgs.TryGetValue("--output", out var arg1)
? Path.Combine(Directory.GetCurrentDirectory(), arg1)
: null;
using (Stream stream = outputPath != null ? File.Create(outputPath) : Console.OpenStandardOutput())
using (var streamWriter = new FormattingStreamWriter(stream, CultureInfo.InvariantCulture))
{
IOpenApiWriter writer;
if (namedArgs.ContainsKey("--yaml"))
writer = new OpenApiYamlWriter(streamWriter);
else
writer = new OpenApiJsonWriter(streamWriter);
if (namedArgs.ContainsKey("--serializeasv2"))
{
if (swaggerDocumentSerializer != null)
{
swaggerDocumentSerializer.SerializeDocument(swagger, writer, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0);
}
else
{
swagger.SerializeAsV2(writer);
}
}
else if (swaggerDocumentSerializer != null)
{
swaggerDocumentSerializer.SerializeDocument(swagger, writer, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0);
}
else
{
swagger.SerializeAsV3(writer);
}
if (outputPath != null)
Console.WriteLine($"Swagger JSON/YAML successfully written to {outputPath}");
}
return 0;
});
});
return runner.Run(args);
}
private static string EscapePath(string path)
{
return path.Contains(" ")
? "\"" + path + "\""
: path;
}
private static IServiceProvider GetServiceProvider(Assembly startupAssembly)
{
if (TryGetCustomHost(startupAssembly, "SwaggerHostFactory", "CreateHost", out IHost host))
{
return host.Services;
}
if (TryGetCustomHost(startupAssembly, "SwaggerWebHostFactory", "CreateWebHost", out IWebHost webHost))
{
return webHost.Services;
}
try
{
return WebHost.CreateDefaultBuilder()
.UseStartup(startupAssembly.GetName().Name)
.Build()
.Services;
}
catch
{
var serviceProvider = HostingApplication.GetServiceProvider(startupAssembly);
if (serviceProvider != null)
{
return serviceProvider;
}
throw;
}
}
private static bool TryGetCustomHost<THost>(
Assembly startupAssembly,
string factoryClassName,
string factoryMethodName,
out THost host)
{
// Scan the assembly for any types that match the provided naming convention
var factoryTypes = startupAssembly.DefinedTypes
.Where(t => t.Name == factoryClassName)
.ToList();
if (!factoryTypes.Any())
{
host = default;
return false;
}
if (factoryTypes.Count() > 1)
throw new InvalidOperationException($"Multiple {factoryClassName} classes detected");
var factoryMethod = factoryTypes
.Single()
.GetMethod(factoryMethodName, BindingFlags.Public | BindingFlags.Static);
if (factoryMethod == null || factoryMethod.ReturnType != typeof(THost))
throw new InvalidOperationException(
$"{factoryClassName} class detected but does not contain a public static method " +
$"called {factoryMethodName} with return type {typeof(THost).Name}");
host = (THost)factoryMethod.Invoke(null, null);
return true;
}
}
}