Swagger is a great thing! It allows us to easily see the API of our service, generate a client for it in different languages and even work with the service through the UI. In ASP.NET Core we have NuGet package Swashbuckle.AspNetCore for the support of Swagger.
But there is one thing I don't like about this implementation. Swashbuckle can show me descriptions of methods, parameters, and classes based on XML comments in the .NET code. But it does not show the descriptions of the enum members.
Let me show you what I mean.
Service creation
I created a simple Web service:
/// <summary>
/// Contains endpoints that use different enums.
/// </summary>
[Route("api/data")]
[ApiController]
public class EnumsController : ControllerBase
{
/// <summary>
/// Executes operation of requested type and returns result status.
/// </summary>
/// <param name="id">Operation id.</param>
/// <param name="type">Operation type.</param>
/// <returns>Result status.</returns>
[HttpGet]
public Task<Result> ExecuteOperation(int id, OperationType type)
{
return Task.FromResult(Result.Success);
}
/// <summary>
/// Changes data
/// </summary>
[HttpPost]
public Task<IActionResult> Change(DataChange change)
{
return Task.FromResult<IActionResult>(Ok());
}
}
This controller makes extensive use of enums. It uses them as argument types, as method results, and as parts of more complex objects:
/// <summary>
/// Operation types.
/// </summary>
public enum OperationType
{
/// <summary>
/// Do operation.
/// </summary>
Do,
/// <summary>
/// Undo operation.
/// </summary>
Undo
}
/// <summary>
/// Operation results.
/// </summary>
public enum Result
{
/// <summary>
/// Operations was completed successfully.
/// </summary>
Success,
/// <summary>
/// Operation failed.
/// </summary>
Failure
}
/// <summary>
/// Data change information.
/// </summary>
public class DataChange
{
/// <summary>
/// Data id.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Source type.
/// </summary>
public Sources Source { get; set; }
/// <summary>
/// Operation type.
/// </summary>
public OperationType Operation { get; set; }
}
/// <summary>
/// Types of sources.
/// </summary>
public enum Sources
{
/// <summary>
/// In-memory data source.
/// </summary>
Memory,
/// <summary>
/// Database data source.
/// </summary>
Database
}
I installed Swashbuckle.AspNetCore NuGet package to support Swagger. Now I must configure it. It can be done in the Startup file:
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c => {
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI();
app.UseRouting();
...
}
}
Now we can start our service. And at the address http://localhost:5000/swagger/index.html we'll find a description of it:
But now all our enumerations are represented by mere numbers:
I'd prefer to provide string values for enumerations. They at least make some sense to users, unlike these numbers.
To do this, we need to make some changes to the Swashbuckle configuration. I installed another NuGet package Swashbuckle.AspNetCore.Newtonsoft. And here are my changes. I changed
services.AddControllers();
to
services.AddControllers().AddNewtonsoftJson(o =>
{
o.SerializerSettings.Converters.Add(new StringEnumConverter
{
CamelCaseText = true
});
});
Now our enumerations are represented as strings:
But even now I see one drawback. Swagger UI does not show me XML comments assigned to the members of enumerations.
Description of enumeration types
Let's see how we can get them. I did a bit of searching on the internet but found almost nothing. Although there is one very interesting piece of code. Unfortunately, it matches the old version of Swashbuckle. Nevertheless, it is a good starting point.
Swashbuckle allows us to interfere with the documentation generation process. For example, there is an interface ISchemaFilter, which allows you to change the schema description of individual classes. The following code shows how to change the descriptions of enumerations:
public class EnumTypesSchemaFilter : ISchemaFilter
{
private readonly XDocument _xmlComments;
public EnumTypesSchemaFilter(string xmlPath)
{
if(File.Exists(xmlPath))
{
_xmlComments = XDocument.Load(xmlPath);
}
}
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (_xmlComments == null) return;
if(schema.Enum != null && schema.Enum.Count > 0 &&
context.Type != null && context.Type.IsEnum)
{
schema.Description += "<p>Members:</p><ul>";
var fullTypeName = context.Type.FullName;
foreach (var enumMemberName in schema.Enum.OfType<OpenApiString>().Select(v => v.Value))
{
var fullEnumMemberName = $"F:{fullTypeName}.{enumMemberName}";
var enumMemberComments = _xmlComments.Descendants("member")
.FirstOrDefault(m => m.Attribute("name").Value.Equals(fullEnumMemberName, StringComparison.OrdinalIgnoreCase));
if (enumMemberComments == null) continue;
var summary = enumMemberComments.Descendants("summary").FirstOrDefault();
if (summary == null) continue;
schema.Description += $"<li><i>{enumMemberName}</i> - {summary.Value.Trim()}</li>";
}
schema.Description += "</ul>";
}
}
}
The constructor of this class accepts a path to the file with XML comments. I read its contents into the XDocument object. Then in Apply method, we check if the current type is enumeration. For such types, we add an HTML list with descriptions of all the members of this enumeration to the type description.
Now we must plug the class of our filter into Swashbuckle:
services.AddSwaggerGen(c => {
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
c.SchemaFilter<EnumTypesSchemaFilter>(xmlPath);
});
It can be done using the SchemaFilter method in the configuration section for Swagger. I pass the path to the file with XML comments to this method. This value will be passed to the constructor of the EnumTypesSchemaFilter class.
Now the Swagger UI shows the enum descriptions as follows:
Description of enumeration parameters
It looks better. But not good enough. Our controller has a method that takes an enum as a parameter:
public Task<Result> ExecuteOperation(int id, OperationType type)
Let's see how the Swagger UI shows this:
As you can see, there is no description of the enum members here. The reason is that we see here a description of the parameter, not a description of the parameter type. So this is an XML comment for the parameter, not for the parameter type.
But we can solve this problem too. To do this, we will use another Swashbuckle interface - IDocumentFilter. Here is our implementation:
public class EnumTypesDocumentFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
foreach (var path in swaggerDoc.Paths.Values)
{
foreach(var operation in path.Operations.Values)
{
foreach(var parameter in operation.Parameters)
{
var schemaReferenceId = parameter.Schema.Reference?.Id;
if (string.IsNullOrEmpty(schemaReferenceId)) continue;
var schema = context.SchemaRepository.Schemas[schemaReferenceId];
if (schema.Enum == null || schema.Enum.Count == 0) continue;
parameter.Description += "<p>Variants:</p>";
int cutStart = schema.Description.IndexOf("<ul>");
int cutEnd = schema.Description.IndexOf("</ul>") + 5;
parameter.Description += schema.Description
.Substring(cutStart, cutEnd - cutStart);
}
}
}
}
}
Here, in the Apply method, we iterate through all the parameters of all the methods of all the controllers. Unfortunately, in this interface, we do not have access to the parameter type, only to the schema of this type (at least I think so). That's why I just cut the description of the enum members from the string with the parameter type description.
Our class must be registered in the same way using the DocumentFilter method:
services.AddSwaggerGen(c => {
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
c.SchemaFilter<EnumTypesSchemaFilter>(xmlPath);
c.DocumentFilter<EnumTypesDocumentFilter>();
});
Here's what the parameter description in the Swagger UI looks like now:
Conclusion
The code presented in this article is more of a sketch than a final version. But I hope it can be useful and allow you to add a description of the enum members to your Swagger UI. Thank you!
P.S. You can find the whole code of the project on GitHub.
This saved me many hours. Thank you for posting!
ReplyDeleteOne trivial additional requirement is to also include
Delete> services.AddSwaggerGenNewtonsoftSupport();
after the call to services.AddSwaggerGen(...)