I just introduced Swashbuckle to a project I've been working on. If you're not familiar with Swashbuckle and/or Swagger UI, you can check them out
here and
here respectively. They work together to provide really simple out of the box documentation, which you can configure in a number of ways.
In my case I wanted to allow markdown in my XML comments that would then be displayed to my user. I also wanted to be able to control access to my API documentation. That may seem counter-intuitive (why document it if you're going to restrict access?) so you'll just have to trust me that it was necessary.
Both of these requirements proved to be pretty simple once I found the right posts and put them all together. I figured I'd centralize them so next time I have to do this I have all the information in one place.
Before we get into configuration and tweaking, you'll need to actually install Swagger, which can be done by adding the Swashbuckle.AspNetCore package from NuGet. If you want to follow these instructions, you'll also need to install the Microsoft.Extensions.PlatformAbstractions.
For the markdown in my XML, I had to take the following steps:
- Configure my project to generate XML comment documentation
- Configure Swagger UI to use the generated XML document
- Create an operation filter to format comments as markdown
- Set Swagger UI to use the new filter
Configuring the project to generate XML comment documentation is pretty easy. Right-click on your project and choose Properties. On the Build tab, check "XML documentation file" and in the textbox enter the name and path you want your file to be generated into. For this post I used "bin\Debug\net461\my-awesome-comments.xml" (without the quotes of course).
Assuming you go with the out-of-the-box, most-simple usage of Swagger UI to get started, your startup.cs includes something like this:
1: public void ConfigureServices(IServiceCollection services)
2: {
3: ...snip...
4: services.AddSwaggerGen(c =>
5: {
6: c.SwaggerDoc("v1", new Info {Title = "Identity Server", Version = "v1"});
7: });
8: ...snip...
9: }
To configure Swagger UI to use the generated XML document you have to two lines to your AddSwaggerGen invocation. After our changes we have this:
1: public void ConfigureServices(IServiceCollection services)
2: {
3: ...snip...
4: services.AddSwaggerGen(c =>
5: {
6: c.SwaggerDoc("v1", new Info {Title = "Identity Server", Version = "v1"});
7:
8: var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "my-awesome-comments.xml");
9: c.IncludeXmlComments(filePath);
10: });
11: ...snip...
12: }
That's it. Our Swagger UI now uses our XML comments. We're not able to use markdown yet, but we're getting there.
I'm not super up to speed on operation filters, but I found this solution in an answer to an
issue someone opened on the Github repo for Swashbuckle. Scroll down to the answer on April 23, 2015 from user
geokaps. I like having everything separated in folder structures, but you don't necessarily have to do it that way. In my case, I created a folder called Filters and created a new file in that folder called FormatXmlCommentSwaggerFilter.cs. Here are the contents of that file:
1: public class FormatXmlCommentSwaggerFilter : IOperationFilter
2: {
3: public void Apply(Operation operation, OperationFilterContext context)
4: {
5: operation.Description = Formatted(operation.Description);
6: operation.Summary = Formatted(operation.Summary);
7: }
8:
9: private string Formatted(string text)
10: {
11: if (text == null) return null;
12:
13:
14:
15: string resultString = Regex.Replace(text, @"^[ \t]+[^<]*>|[^>]*<\/", "", RegexOptions.Multiline);
16: resultString = Regex.Replace(resultString, @"<code[^>]*>", "<pre>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Multiline);
17: resultString = Regex.Replace(resultString, @"</code[^>]*>", "</pre>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Multiline);
18: resultString = Regex.Replace(resultString, @"<!--", "", RegexOptions.Multiline);
19: resultString = Regex.Replace(resultString, @"-->", "", RegexOptions.Multiline);
20:
21: try
22: {
23: string pattern = @"<pre\b[^>]*>(.*?)</pre>";
24:
25: foreach (Match match in Regex.Matches(resultString, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Multiline))
26: {
27: var formattedPreBlock = FormatPreBlock(match.Value);
28: resultString = resultString.Replace(match.Value, formattedPreBlock);
29: }
30: return resultString;
31: }
32: catch
33: {
34:
35: return resultString;
36: }
37: }
38:
39: private string FormatPreBlock(string preBlock)
40: {
41:
42: var linesArray = preBlock.Split('\n');
43: if (linesArray.Length < 2)
44: {
45: return preBlock;
46: }
47: else
48: {
49:
50: string line = linesArray[1];
51: int lineLength = line.Length;
52: string formattedLine = line.TrimStart(' ', '\t');
53: int paddingLength = lineLength - formattedLine.Length;
54:
55:
56: for (int i = 1; i < linesArray.Length - 1; i++)
57: {
58: linesArray[i] = linesArray[i].Substring(paddingLength);
59: }
60:
61: var formattedPreBlock = string.Join("", linesArray);
62: return formattedPreBlock;
63: }
64: }
65: }
Once I had that filter created I just had to modify my Swagger UI configuration to use it. After these changes here's my whole Swagger UI configuration in startup.cs:
1: public void ConfigureServices(IServiceCollection services)
2: {
3: ...snip...
4: services.AddSwaggerGen(c =>
5: {
6: c.SwaggerDoc("v1", new Info {Title = "Identity Server", Version = "v1"});
7:
8: var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "my-awesome-comments.xml");
9: c.IncludeXmlComments(filePath);
10: c.OperationFilter<FormatXmlCommentSwaggerFilter>();
11: });
12: ...snip...
13: }
That's all I had to do to enable markdown in my XML comments and it works pretty well I have to say. The next part was a little bit trickier because I wanted to lock down my entire Swagger instance so that only authorized users (with a particular permission) would be able to access it. To do this, I took the following steps:
- Create Swagger authorization middleware
- Create an extension method to use the middleware
- Use extension method to wire up the middleware
In my particular scenario I'm using Swagger to document our Identity Server API so I already had the ability to secure my other endpoints. On the surface it should have been just as straightforward to secure my Swagger endpoints. But I didn't want just anyone to be able to authenticate with our Identity Server and then view my APIs. I wanted to keep those private so only certain people (developers in my organization) could see the APIs after they're authenticated. To do that I created the following middleware to validate that the user is authenticated (logged in) and also should be able to access Swagger:
1: public class SwaggerAuthorizedMiddleware
2: {
3: private readonly RequestDelegate _next;
4:
5: public SwaggerAuthorizedMiddleware(RequestDelegate next)
6: {
7: _next = next;
8: }
9:
10: public async Task Invoke(HttpContext context)
11: {
12:
13: if (context.Request.Path.Equals("/swagger/index.html")
14: && (!context.User.Identity.IsAuthenticated || context.User.Claims.All(c => c.Type != "can_access_swagger") ||
15: context.User.Claims.First(c => c.Type == "can_access_swagger").Value != "true"))
16: {
17:
18:
19: await context.ChallengeAsync();
20: return;
21: }
22:
23:
24:
25: await _next.Invoke(context);
26: }
27: }
That's all the middleware has to do. Now, keep in mind that we already had our authentication setup and I'm just plugging into that existing authentication and checking whether the user is authenticated and has access. If you don't already have that setup (i.e. your API is not already secured) then securing your Swagger UI is going to be more complicated. Even if that's the case, hopefully this helps steer you in the right direction.
Now that I have the middleware, I want to create a really simple extension method so I can use the middleware in my startup class
1: public static class SwaggerAuthorizeExtensions
2: {
3: public static IApplicationBuilder UseSwaggerAuthorized(this IApplicationBuilder builder)
4: {
5: return builder.UseMiddleware<SwaggerAuthorizedMiddleware>();
6: }
7: }
Then once I have that extension method it's just a matter of wiring it up. In startup.cs I have a Configure method. In there, after the UseMvc invocation, I want to add UseSwaggerAuthorized:
app.UseSwaggerAuthorized();
That's it! My Swagger UI is now protected with my pre-existing authentication process with an added check for whether the user should be able to access my Swagger documentation. Hope this helps someone!