The OpenAPI Specification in Azure Functions
If you have worked with SOAP services in your career, surely you have had to deal with the concept of a Web Services Description Language (WSDL) manifest. WSDL is the identity card of a SOAP service and fully describes the service itself in terms of the endpoints, ports, and payloads used by the service. Using WSDL, you can understand how a third-party service works and you can create its client automatically.
The OpenAPI Specification is the counterpart of WSDL for REST API services: they are a set of specifications created by the most important players in the computer science world, such as Google, Microsoft, and IBM which join together in open governance structure under the Linux Foundation. The purpose of the OpenAPI specification is to standardize how REST APIs are described using a programming language-agnostic approach based on YAML or JSON files.
Sometimes, the OpenAPI Specification is also known as Swagger, but they are two different sides of the same coin: OpenAPI is the specifications while Swagger is one of the tools you can use to implement the specifications. To make a comparison with the web world: OpenAPI is comparable to W3C's HTML specifications, while Swagger is comparable to one of the browsers that implement such specifications.
At the time of writing this book, the Azure Functions runtime doesn't yet support a native mode to implement OpenAPI specifications in an HttpTrigger function but there are, however, ways to implement them using third-party NuGet packages.
First of all, consider the following function:
[FunctionName(nameof(GetBooks))]
public static IActionResult GetBooks(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "books")] HttpRequest req,
ILogger log)
{
log.LogInformation($"Call {nameof(GetBooks)}");
var books = BooksRepository.Books;
if (req.Query.ContainsKey("title"))
{
var titleFilter = req.Query["title"];
books = books.Where(b => b.Title.Contains(titleFilter));
}
return (ActionResult)new OkObjectResult(books);
}
The function returns a list of books filtered by title:
GET api/books?title=The%20Great%20Gatsby
[
{
"title":"The Great Gatsby",
"price":10.5,
"author":"F. Scott Fitzgerald"
}
]
The OpenAPI Specification for the function looks like this:
{
"openapi": "3.0.1",
"info": {
....
},
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:7071/api"
}
],
"paths": {
"/books": {
"get": {
"operationId": "books",
"parameters": [
...
],
"responses": {
"200": {
...
}
}
}
}
},
...
}
You have an entry in the path collections for each API exposed and for each entry. As you can see in the previous sample, you have a complete description of the HTTP verb supported (GET in the sample) and all the possible responses supported by the API.
To implement the OpenAPI Specification in the previous Azure Function, you have, at this moment, to use a third-party package. In the following sample, we use the NuGet package called Aliencube.AzureFunctions.Extensions.OpenApi. You will find many packages that allow you to implement the OpenAPI Specification in your functions, but we've chosen that because it is simple, it is under MIT license, the code is on GitHub, and it allows you to understand what the steps you need to implement are to expose the JSON (or YAML) file with the OpenAPI description.
The first thing you have to do is to install the package in your Azure Functions project, for example, using the following command in the Package Manager Console (inside Visual Studio):
Install-Package Aliencube.AzureFunctions.Extensions.OpenApi
Once the package is installed, you need to decorate your function so that the package scaffolding procedure can retrieve the information needed to generate the OpenAPI specification file. The OpenAPI package implements a set of attributes that allow you to define the metadata of the function, its input parameters, and its responses:
[FunctionName(nameof(GetBooks))]
[OpenApiOperation("books", Description = "Retrieves a list of books filtered by the title")]
[OpenApiParameter("title", In = ParameterLocation.Query, Required = false, Type = typeof(string))]
[OpenApiResponseBody(HttpStatusCode.OK, "application/json", typeof(IEnumerable<BookModel>), Description ="The books that contains the filter in the title")]
public static IActionResult GetBooks(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "books")] HttpRequest req,
ILogger log)
{
log.LogInformation($"Call {nameof(GetBooks)}");
var books = BooksRepository.Books;
if (req.Query.ContainsKey("title"))
{
var titleFilter = req.Query["title"];
books = books.Where(b => b.Title.Contains(titleFilter));
}
return (ActionResult)new OkObjectResult(books);
}
As you can see, you can add the following attributes:
- OpenApiOperation: This provides some path information, such as the operation ID and the description.
- OpenApiParameter: This defines information about an input parameter, such as the name, the direction, the location in the request (for example, a query string), the type, and whether the parameter is mandatory or not. Of course, you must have one attribute for each parameter that your function supports.
- OpenApiResponseBody: This defines information about the function response, such as the content type, the type, and the description. You should have one attribute for each status code response your function supports.
In the previous snippet, the BookRepository class simulates the data access layer (using it, you can retrieve the full list of books available). You can find its implementation in the GitHub repository for the chapter.
Correctly decorating all functions, however, is not enough because you must implement an endpoint that generates the OpenAPI Specification file. To do this, you can use another function of type HttpTrigger:
[FunctionName(nameof(RenderOpenApiDocument))]
[OpenApiIgnore]
public static async Task<IActionResult> RenderOpenApiDocument(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "openapi")] HttpRequest req,
ILogger log)
{
var ver = OpenApiSpecVersion.OpenApi3_0;
var ext = OpenApiFormat.Json;
var settings = new AppSettings();
var helper = new DocumentHelper();
var document = new Document(helper);
var result = await document.InitialiseDocument()
.AddMetadata(settings.OpenApiInfo)
.AddServer(req, settings.HttpSettings.RoutePrefix)
.Build(Assembly.GetExecutingAssembly())
.RenderAsync(ver, ext)
.ConfigureAwait(false);
var response = new ContentResult()
{
Content = result,
ContentType = "application/json",
StatusCode = (int)HttpStatusCode.OK
};
return response;
}
The function uses the scaffolding features exposed by the Aliencube package to generate (the Build method) and render (the RenderAsync method) the OpenAPI Specification file and return it as JSON output. In the function, we choose the OpenAPI v3.0 version and JSON format but you can change these options using one of the supporting values of OpenApiSpecVersion and OpenApiFormat enumeration. You will notice the use of the AppSetting class in the previous sample:
class AppSettings : OpenApiAppSettingsBase
{
public AppSettings() : base()
{
}
}
That class allows you to read the info section for the OpenAPI specification from the app settings:
{
...
"Values": {
...
"OpenApi__Info__Version": "1.0.0",
"OpenApi__Info__Title": "OpenAPISupport version 1.0.0",
"OpenApi__Info__Description": "A simple sample to configure OpenAPI on Azure Functions.",
"OpenApi__Info__Contact__Url": "https://github.com/masteringserverless/CH01-OpenAPI",
"OpenApi__Info__License__Name": "MIT",
"OpenApi__Info__License__Url": "http://opensource.org/licenses/MIT"
}
}
Finally, you complete the OpenAPI render function implementation decorated with the OpenApiIgnore attribute, which excludes the function from the scaffolding process (so you will not see the render function in the OpenAPI specification file).
If you call the function using, for example, a browser, you receive the following result:
In the next section, we will be using ngrok.