Introduction
Keeping documentation updated is arguebly one of the hardest issues in software development. Maintaining great documentation for web apis is simplified with great tooling for the OpenApi (Swagger) standard like Swashbuckle for .NET projects. One of the limitations of OpenApi 2 was the ability to generate types based on inheritance and polymorphism and this is fixed in OpenApi 3.
We will go through the basic setup of Swashbuckle for .NET and then an example of how to have Swagger generator define classes that can be returned from an api based on an initial type and emitting the oneOf return types in the OpenApi definition. This gives the client a more complete representation of the types that can be returned to include any derived types from the base class being returned by the service(s).
Complete source code to this example is found here
1.) Getting Started with Swashbuckle for .NET
A more in depth getting started is found in the Microsoft documentation for Swashbuckle, so we will just hit the highlights here.
Project Reference
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0" />
</ItemGroup>
Startup.cs
using Microsoft.OpenApi.Models;
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "Simple API",
Description = "A simple example ASP.NET Core Web API",
TermsOfService = new Uri("https://elcamino.cloud/privacy.html"),
Contact = new OpenApiContact
{
Name = "Dave Melendez",
Email = string.Empty,
Url = new Uri("https://elcamino.cloud"),
},
License = new OpenApiLicense
{
Name = "Use under LICX",
Url =
new Uri("https://github.com/dlmelendez/elcamino-cloud-blog-companion/blob/master/LICENSE"),
}
});
});
public void Configure(IApplicationBuilder app)
{
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Add the Swagger UI for viewing OpenApi document and testing
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.RoutePrefix = string.Empty; // This puts the swagger UI at the root of the web app
});
WeatherForecast Example v1
Check out v1 tag of github code here for the work done so far. Executing this project at this time should give you a SwaggerUI for the WeatherForecast something like this:
2.) Inheritance and Polymorphism in OpenApi
Now, let's add another MVC controller that returns a base type DynamicType but the controller action will actually return a derived class type DynamicType<T>
Model Class Hierarchy and Controller Action
using System;
namespace OpenApiWebAppExample
{
public class DynamicType
{
public string DataType { get; set; }
}
public class DynamicType<T> : DynamicType
{
public T Value { get; set; }
}
}
Note
So why do we include the DataType property? This is known as the Discriminator property that will tell the client what type of object to expect. In following case, we will set DataType to 'string' or 'number' to define the Value property type to the client. This Discriminator property is emitted in the OpenApi document that is generated.
namespace OpenApiWebAppExample.Controllers
{
[ApiController]
[Produces("application/json")]
[Route("[controller]")]
public class PolymorphismController : ControllerBase
{
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<DynamicType>), StatusCodes.Status200OK)]
public IEnumerable<DynamicType> Get()
{
return Enumerable.Range(1, 5).Select(index => {
if(index % 2 == 0)
{
return new DynamicType<string>()
{
DataType="string",
Value = "This is my string value"
} as DynamicType;
}
return new DynamicType<decimal?>()
{
DataType="number",
Value = new decimal?(99.0m)
} as DynamicType;
})
.ToArray();
}
}
}
Warning
Notice the attributes Produces and ProducesResponseType help to concretely define the content type and http response types, respectively. You might ask, why don't you just add the additional response types of DynamicType<T> here? Well, you can only define one ProducesResponseType per http status code and duplicates of the http response code will be ignored.
Checking our progress
Check out v2 tag of github code here for the work done so far. At this point, the application will run and the new PolymorphismController action Get should work. However, when we inspect the SwaggerUI, the DynamicType<T> is not found and the only reference for the client is the DynamicType base class which does not represent the response of the controller fully. We are not done yet, but here it is so far.
Adding the Polymorphism schema to the OpenApi Document
Let's revisit the Startup.cs to now include the mapping for the DynamicType<string> and DynamicType<decimal?> types we are returning. We do this by adding this configuration to the Swagger generator.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c =>
{
c.GeneratePolymorphicSchemas(infoType => {
if(infoType == typeof(DynamicType))
{
return new Type[] {
typeof(DynamicType<string>),
typeof(DynamicType<decimal?>)
};
}
return Enumerable.Empty<Type>();
}, (discriminator) => {
if(discriminator == typeof(DynamicType))
{
return "dataType";
}
return null;
});
//
c.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "Simple API",
Description = "A simple example ASP.NET Core Web API",
TermsOfService = new Uri("https://elcamino.cloud/privacy.html"),
Contact = new OpenApiContact
{
Name = "Dave Melendez",
Email = string.Empty,
Url = new Uri("https://elcamino.cloud"),
},
License = new OpenApiLicense
{
Name = "Use under LICX",
Url =
new Uri("https://github.com/dlmelendez/elcamino-cloud-blog-companion/blob/master/LICENSE"),
}
});
});
}
Now from the configuration, you can see we are defining the derived class possibilities as well as the discriminator value for the client to evaluate the Value type correctly.
Note
The DynamicType<T> Value property now shows as part of the sample response.
From the https://localhost:5001/swagger/v1/swagger.json OpenApi document that was generated (abbrevatied)
{
"openapi": "3.0.1",
"info": {
"title": "Simple API",
"description": "A simple example ASP.NET Core Web API",
"termsOfService": "https://elcamino.cloud/privacy.html",
"contact": {
"name": "Dave Melendez",
"url": "https://elcamino.cloud",
"email": ""
},
"license": {
"name": "Use under LICX",
"url": "https://github.com/dlmelendez/elcamino-cloud-blog-companion/blob/master/LICENSE"
},
"version": "v1"
},
"paths": {
"/Polymorphism": {
"get": {
"tags": [
"Polymorphism"
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/StringDynamicType"
},
{
"$ref": "#/components/schemas/DecimalNullableDynamicType"
}
]
}
}
...
"components": {
"schemas": {
"DynamicType": {
"required": [
"dataType"
],
"type": "object",
"properties": {
"dataType": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false,
"discriminator": {
"propertyName": "dataType"
}
},
"StringDynamicType": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/DynamicType"
}
],
"properties": {
"value": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"DecimalNullableDynamicType": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/DynamicType"
}
],
"properties": {
"value": {
"type": "number",
"format": "double",
"nullable": true
}
},
"additionalProperties": false
},
Note
The DynamicType<string> and DynamicType<decimal?> schemas now show up and the schemas are referenced in the OpenApi document. Check out the final version on github.
Summary
This article was intended to give the highlights OpenApi (Swagger), Swashbuckle for .NET and how to handle polymorphism with OpenApi.
- Getting started with Swashbuckle for .NET Core
- A full blown example of polymorphism and the configuration needed to generate the OpenApi document for client consumption.
Open a discussion Tweet