Deprecated Actions III: Decorating with OpenAPI

In the previous blogs in this series, we created an azure function and custom connector and then updated the function to work more cleanly with the Power Platform. In this blog we’re going to decorate our azure function so that we can more easily create the OpenAPI definition used by the custom connector.

Scenario

Our durable function exists and is working nicely. We created a custom connector, but we had to carefully configure the expected inputs and outputs to match the azure function. Let’s decorate the code so that an OpenAPI definition (aka Swagger definition) can be automatically generated, thus simplifying the maintenance of the custom connector.

Azure Functions OpenAPI Extension

The ability to decorate an azure function has been around for a while. Initially developed by Justin Yoo it has now been adopted by Microsoft and is currently in preview status (0.81-preview). The Azure Functions OpenAPI extension github repo contains excellent documentation, and there are also examples on Justin’s blog.

Decorating the functions

We must first add the Microsoft.Azure.WebJobs.Extensions.OpenApi nuget package to our project via dotnet add package --prerelease Microsoft.Azure.WebJobs.Extensions.OpenApi or using Visual Studio. Currently, we must use the –prerelease flag. Next, add the necessary ‘using’s shown below:

using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums;
using Microsoft.OpenApi.Models;

Decorating the HTTP trigger function

We want to expose the HTTP trigger function (see part 1 of the series) as an action in our connector, therefore we’ll add the following decorators:

  • OpenApiOperation defines the unique operationId, summary and description properties which will be shown when using the action in the custom connector. A ‘Visibility’ property of ‘Important’ indicates that action will always be shown in the cloud flow editor.
  • OpenApiSecurity to specify the security method.
  • OpenApiRequestBody to indicate that the triggering function accepts a request comprising a JSON object in the format of the serialised ScrapeConnectorsRequest class.
  • OpenApiResponseWithBody to indicate that upon successful completion, the output will be a JSON list of ‘ConnectorInfo’ objects.
[OpenApiOperation(operationId: nameof(ScrapeConnectors), tags: new[] { "default" },
                  Summary = "Get Actions", Description = "Gets the actions and their deprecation status for one or more custom connectors",
                  Visibility = OpenApiVisibilityType.Important)]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "x-functions-key", In = OpenApiSecurityLocationType.Header)]
[OpenApiRequestBody(contentType: "application/json",  typeof(ScrapeConnectorsRequest), Required = true)]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(List<ConnectorInfo>), Summary = "Connector information", Description = "The actions/operations within a connector and the status thereof")]
[FunctionName(nameof(ScrapeConnectors))]
public static async Task<IActionResult> ScrapeConnectors(
    ... snip ...

Decorating the status function

As covered in the part 2 of the series this is the key function needed to enable the Power Platform to work seamlessly with durable functions. Even though this is an internal function it still needs to be made known to the connector. We will decorate it with OpenApiVisibilityType.Internal so that it is not displayed in the power platform user interface. We state that the instanceId will be a required parameter on the path.

[OpenApiOperation(operationId: nameof(ScrapeConnectorsStatus), tags: new[] { "default" },
                  Summary = "Get Status of Durable Function", Description = "Gets the status of a durable function",
                  Visibility = OpenApiVisibilityType.Internal)]
[OpenApiParameter(name: "instanceId", In = ParameterLocation.Path, Required = true, Type = typeof(string))]
[OpenApiResponseWithoutBody(statusCode: HttpStatusCode.OK, Description = "default")]
[FunctionName(nameof(ScrapeConnectorsStatus))]
public static async Task<IActionResult> ScrapeConnectorsStatus(
    ... snip ...

As a minimum that’s all we need to do. The github docs explain how to customise the header, title, theme, security and much more.

Debugging locally

If we run the azure function locally, we see that new endpoints have been added.

endpoints

It’s particulary nice that we can access the SwaggerUI at http://localhost:7071/api/swagger/ui.

SwaggerUI

Finally we can get the OpenAPI definition in JSON format via https://localhost:7071/api/swagger.json (or YAML by visiting swagger.yaml).

Updating the custom connector

When the azure function is deployed to the cloud both the OpenAPI definition and SwaggerUI are still available (this can be turned off if needed). In this example the OpenAPI definition is at https://funcdeprecatedactionsblog.azurewebsites.net/api/swagger.yaml. By toggling the ‘Swagger Editor’ switch we can copy and paste the YAML into the custom connector, as shown below:

Updating custom connector

This makes it much easier to keep the custom connector definition in sync with the azure function. There’s much more documentation on this process in the Create a custom connector from an OpenAPI definition documentation.

Testing in Power Automate

When we try to use our custom connector, we now see our ScrapeConnectors function exposed via the ‘Get Actions’ action with the summary and description taken from the OpenAPI definition. The ScrapeConnectorsStatus function isn’t displayed to the user because we set the visibility to ‘Internal’.

Adding Action

Power Automate now knows about the structure of the response that will be output by the action. This is due to the models contained within the OpenAPI definition that have been derived from our c# response classes.

Output structure

Summary

  • We’ve simplified the creation of a custom connector by decorating our azure function.
  • The OpenAPI definition is generated by the azure function, so ‘translation errors’ are less likely.
  • We no longer have to manually edit the custom connector, just copy and paste in the generated YAML.

Improvements

The Azure Functions OpenAPI extension package doesn’t currently generate all possible OpenAPI definitions for a custom connector. However, the following github issues indicate this could happen in the fullness of time.

During development, it would be great to automatically update the custom connector at the same time the Azure function is updated. Currently this is still a manual copy & paste.

Microsoft suggest using Azure API Management to centralise APIs and make available to the power platform rather than directly editing the custom connector.

Source Code

You can get the source code and solutions used on my github in the blog3 branch. Import the ‘DeprecatedConnectors’ solution before the ‘DeprecatedConnectorsFlow’ solution due to a known limitation that custom connectors must be installed first.

References