DISCLAIMER: This post is purely a personal opinion, not representing or affiliating my employer's.
Azure Function 1.x provides a preview feature to render Open API 2 (Swagger) definitions. I wrote a blog post about this quite a while ago, but unfortunately, Azure Functions 2.x hasn't yet supported this feature. Therefore, we can't automagically generate it but manually implement to render it. Throughout this post, I'm going to walk through how to render Swagger definition in both JSON and YAML.
All sample codes used in this post can be found at here.
API Design-First vs Implementation-First
In terms of the API Implementation-First approach, Azure Functions 1.x only provides its limited feature as preview. It automatically generates Swagger definitions (although it doesn't perfectly generate it) from the portal by identifying HTTP triggers. As Azure Functions 2.x has omitted this feature, we're not able to use the Implementation-First approach any longer.
On the other hand, if we take the API Design-First approach, we can still render the definitions through another Azure Functions endpoint. This Design-First approach is also beneficial for collaborations between front-end and back-end developers, even though back-end developers haven't implemented it because front-end developers already know API structures. Therefore, many enterprise-wide development prefer this API Design-First approach.
The sample code above has already got the Swagger definitions in YAML format, so we can simply use this.
swagger: "2.0" | |
info: | |
description: "This is a custom Key Vault connector for Logic App." | |
version: "1.0.0" | |
title: "Key Vault Connector" | |
termsOfService: "https://raw.githubusercontent.com/aliencube/Key-Vault-Connector-for-Logic-Apps/master/LICENSE" | |
contact: | |
url: "https://github.com/aliencube/Key-Vault-Connector-for-Logic-Apps/issues" | |
license: | |
name: "MIT" | |
url: "https://raw.githubusercontent.com/aliencube/Key-Vault-Connector-for-Logic-Apps/master/LICENSE" | |
externalDocs: | |
description: "Find out more about Key Vault Custom Connector" | |
url: "https://github.com/aliencube/Key-Vault-Connector-for-Logic-Apps" | |
host: "keyvaultconnector.azurewebsites.net" | |
basePath: "/api" | |
tags: | |
- name: "secrets" | |
description: "Access to the list of secrets" | |
externalDocs: | |
description: "Find out more" | |
url: "https://github.com/aliencube/Key-Vault-Connector-for-Logic-Apps" | |
- name: "secret" | |
description: "Access to the secret" | |
externalDocs: | |
description: "Find out more" | |
url: "https://github.com/aliencube/Key-Vault-Connector-for-Logic-Apps" | |
schemes: | |
- "https" | |
paths: | |
/secrets: | |
get: | |
tags: | |
- "secrets" | |
summary: "Gets the list of secretes" | |
description: "This returns the list of secrets without their values" | |
operationId: "Secrets" | |
produces: | |
- "application/json" | |
x-ms-visibility: "important" | |
responses: | |
200: | |
description: "OK" | |
schema: | |
type: "array" | |
items: | |
$ref: "#/definitions/SecretItem" | |
404: | |
description: "Not Found" | |
schema: | |
$ref: "#/definitions/Error" | |
500: | |
description: "Internal Server Error" | |
schema: | |
$ref: "#/definitions/Error" | |
security: | |
- authkey: [] | |
/secrets/{name}: | |
get: | |
tags: | |
- "secret" | |
summary: "Gets the secret details" | |
description: "This returns a secreet details including its value" | |
operationId: "Secret" | |
produces: | |
- "application/json" | |
x-ms-visibility: "important" | |
parameters: | |
- name: "name" | |
in: "path" | |
description: "Secret name" | |
required: true | |
type: "string" | |
x-ms-summary: "Name" | |
x-ms-visibility: "important" | |
responses: | |
200: | |
description: "OK" | |
schema: | |
$ref: "#/definitions/Secret" | |
404: | |
description: "Not Found" | |
schema: | |
$ref: "#/definitions/Error" | |
500: | |
description: "Internal Server Error" | |
schema: | |
$ref: "#/definitions/Error" | |
security: | |
- authkey: [] | |
securityDefinitions: | |
authkey: | |
type: "apiKey" | |
name: "x-functions-key" | |
in: "header" | |
definitions: | |
SecretItem: | |
type: "object" | |
properties: | |
id: | |
type: "string" | |
name: | |
type: "string" | |
enabled: | |
type: "boolean" | |
managed: | |
type: "boolean" | |
contentType: | |
type: "string" | |
recoveryLevel: | |
type: "string" | |
created: | |
type: "string" | |
format: "date-time" | |
updated: | |
type: "string" | |
format: "date-time" | |
expires: | |
type: "string" | |
format: "date-time" | |
notBefore: | |
type: "string" | |
format: "date-time" | |
required: | |
- id | |
- name | |
- enabled | |
- recoveryLevel | |
Secret: | |
type: "object" | |
properties: | |
id: | |
type: "string" | |
name: | |
type: "string" | |
value: | |
type: "string" | |
enabled: | |
type: "boolean" | |
managed: | |
type: "boolean" | |
version: | |
type: "string" | |
contentType: | |
type: "string" | |
recoveryLevel: | |
type: "string" | |
created: | |
type: "string" | |
format: "date-time" | |
updated: | |
type: "string" | |
format: "date-time" | |
expires: | |
type: "string" | |
format: "date-time" | |
notBefore: | |
type: "string" | |
format: "date-time" | |
required: | |
- id | |
- name | |
- enabled | |
- recoveryLevel | |
Error: | |
type: "object" | |
properties: | |
statusCode: | |
type: "integer" | |
message: | |
type: "string" | |
required: | |
- statusCode | |
- message |
Rendering Swagger Definitions
As you can see the definitions above, there are two endpoints, /api/secrets
and /api/secret/{name}
. In addition to them, it also defines how request formats are and how response payloads look like. So, the Functions code is only to read the YAML file and parse it either JSON or YAML format, depending on the request. Let's have a look at the entry point below. It has the endpoint of /api/swagger.{extension}
, which takes either json
or yaml
as an extension.
[FunctionName(nameof(RenderSwagger))] | |
public static async Task<IActionResult> RenderSwagger( | |
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "swagger.{extension}")] HttpRequest req, | |
string extension, | |
ILogger log) | |
{ | |
var result = await Factory.Create<IRenderSwaggerFunction, ILogger>(log) | |
.InvokeAsync<HttpRequest, IActionResult>(req, options) | |
.ConfigureAwait(false); | |
return result; | |
} |
Of course, this code uses the Azure Functions Dependency Injection package, so the main logic for rendering/parsing is done within the IRenderSwaggerFunction
instance. Let's have a look. It reads the API definition from the given URL and render it either JSON or YAML, based on the request extension.
public class RenderSwaggerFunction : FunctionBase<ILogger>, IRenderSwaggerFunction | |
{ | |
private readonly HttpClient _http; | |
... | |
public RenderSwaggerFunction(AppSettings settings, HttpClient http) | |
{ | |
this._http = http ?? throw new ArgumentNullException(nameof(http)); | |
... | |
} | |
public override async Task<TOutput> InvokeAsync<TInput, TOutput>(TInput input, FunctionOptionsBase options = null) | |
{ | |
... | |
// Get Swagger definitions from the given URL. | |
var swagger = await this._http.GetStringAsync(this._settings.Swagger.ImportUrl) | |
.ConfigureAwait(false); | |
// Render as YAML | |
if (IsYaml(opt.Extension)) | |
{ | |
return (TOutput)(IActionResult)new OkObjectResult(swagger); | |
} | |
// Render as JSON | |
if (IsJson(opt.Extension)) | |
{ | |
var deserialiser = new DeserializerBuilder().Build(); | |
var deserialised = deserialiser.Deserialize<dynamic>(swagger); | |
return (TOutput)(IActionResult)new OkObjectResult(deserialised); | |
} | |
throw new InvalidOperationException(); | |
} | |
} |
Let's run this code in our local machine and send a request through Postman. If the request is sent with the json
extension, it renders like this:
This time, it renders with the yaml
extension.
It is noticed that the original document is written in YAML, it renders either JSON or YAML. Actually, this can be updated to read either YAML or JSON, rather than reading only YAML document.
So far, we have walked through how Azure Functions 2.x can render Swagger definition document based on request extension. Over the last a few posts, we've played around Azure Functions and Key Vault. In the next post, I'll put this altogether to create a bigger value.