Power Apps,  Power Automate

Exploring OpenAPI Extensions (Part 2) – Dynamic Values and Friends

In my previous post, we explored how to use the dynamic schema (x-ms-dynamic-schema) to extend the OpenAPI definition of a custom connector. In this post, we will look at using dynamic values (x-ms-dynamic-values) and other document enriching extensions, such as x-ms-summary, to improve the usability and readability of our OpenAPI definition. We will start with a simple OpenAPI definition which we will then extend in a series of steps to apply and see the benefit of each OpenAPI extension.

Before we get started, let’s look at what we currently have. In this Power Automate flow, I’m using actions from a custom connector to retrieve agenda templates for a specific account and agenda type:

We often see this kind of pattern where we need to query and filter dependant values first to use as inputs for subsequent actions. Also, we often see labels of the input parameters left in camel case, e.g. agendaTypeId instead of Agenda Type ID.

For the purpose of this post, the above setup can be reduced to this single action using the dynamic values extension:

Sure, I know what you are thinking. Even with this combined action, the users may still need to query and filter dependency values first to use with the input parameters. That way the flow can be deployed without the need for manual updates for the downstream environments. I would say that this action is recommended if the input values don’t change between environments, or the flow is only maintained in one environment to be used as a productivity tool for a person or a team. With that in mind, let’s get started 😎.

 

Step 1 – Preparation

To demonstrate this, I have built a simple .NET Core API for retrieving agendas for Toastmasters clubs. I’m utilising the Swashbuckle.AspNetCore NuGet package to generate the initial OpenAPI definition for the API. You can learn more about how to do this here for your own Web API.

The following Swagger UI shows the list of operations I’m exposing through this API:

Using the blue link at the top left corner of the Swagger UI, we can download the swagger.json file (a.k.a. OpenApi definition file). We can use this to create our custom connector for Power Automate and Power Apps as documented in this article.

I won’t display the entire swagger.json file in this post as it’s pretty lengthy, but you can view the file here. We will use this as our starting point. To get a clear overview of the contents of the file, I have summarized the top-level sections below:

{
    "swagger": "2.0",
    "info": { ... },
    "host": "yourwebapi.azurewebsites.net",
    "schemes": [ ... ],
    "paths": {
      "/api/Accounts": { ... },
      "/api/Accounts/{accountId}/agendas": { ... },
      "/api/AgendaTemplates": { ... },
      "/api/AgendaTemplates/{agendaTemplateId}": { ... },
      "/api/AgendaTemplates/{agendaTemplateId}/schema": { ... },
      "/api/AgendaTemplates/{agendaTemplateId}/agendas": { ... },
      "/api/AgendaTypes": { ... }
    },
    "definitions": { ... },
    "securityDefinitions": { ... },
    "security": [ ... ]
  }

I’m using XML document comments on the controller actions and models to document the code and leverage it to enrich the OpenAPI definition.

/* In TRH.Toastmasters.Api.proj */
/// <summary>
/// Get agenda templates
/// </summary>
/// <remarks>
/// Returns a list of agenda templates
/// </remarks>
/// <param name="accountId">Specify the unique identifier of the account to filter by</param>
/// <param name="agendaTypeId">Specify the unique identifier of the agenda type to filter by</param>
/// <returns>A list of agenda templates</returns>
[HttpGet(Name = "GetAgendaTemplates")]
[ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
public async Task<ActionResult<IEnumerable<AgendaTemplateDto>>> GetAgendaTemplates([FromQuery] Guid? accountId, [FromQuery] Guid? agendaTypeId)
{
    var query = new GetAgendaTemplateListQuery
    {
        AccountId = accountId,
        AgendaTypeId = agendaTypeId
    };

    return Ok(await _mediator.Send(query));
}

/* In TRH.Toastmasters.Application.proj */
public class AgendaTemplateDto
{
    /// <summary>
    /// Unique identifier of the agenda template
    /// </summary>
    public Guid Id { get; set; }

    /// <summary>
    /// Name of the agenda template
    /// </summary>
    public string? Name { get; set; }
    ...
}
Note: In order to keep the documenting style consistent across different VS projects without introducing cross-cutting dependency, there was a conscious decision to not use Swashbuckle.AspNetCore.Annotations NuGet package to decorate the code. 

By using the XML documentation comments, descriptions for the controller actions, action parameters and action response models are automatically extracted and incorporated into the generated swagger.json file. This option is enabled via the Visual Studio project properties under Build > Output:

Or by simply editing the .proj file and including the highlighted lines:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <GenerateDocumentationFile>True</GenerateDocumentationFile>
    <NoWarn>$(NoWarn);1591</NoWarn>
    <DocumentationFile>$(MSBuildProjectName).xml</DocumentationFile>
  </PropertyGroup>
  ...
</Project>

The following code must be added when configuring the Swagger generator in the Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddSwaggerGen(c =>
    {
        ...
        c.IncludeXmlComments(Path.Combine(
            AppContext.BaseDirectory, 
            $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"
            )
        );
        // This demonstrates that you can include XML documentations from other VS projects as well.
        // In our case, this contains the XML comments for the response objects (DTOs).
        c.IncludeXmlComments(Path.Combine(
            AppContext.BaseDirectory, 
            $"{typeof(ApplicationServiceRegistration).Assembly.GetName().Name}.xml"
            )
        );
        ...
    });
    ...
}

If you want to read more detailed explanations of enabling the XML comments, you can check out the Microsoft documentation page here.

In the next couple of steps, we will make the OpenAPI definition more user-friendly before diving into the dynamic values extension. You can go directly to Step 4 if you want to skip these parts.

 

Step 2 – Add x-ms-summary

The purpose of the x-ms-summary extension is to provide the title for the action parameters and response schema. The result will look like this:

We can modify our swagger.json to add x-ms-summary for our action parameters:

{
    ...
    "paths": {
      "/api/AgendaTemplates": {
        "get": {
          "tags": [
            "AgendaTemplates"
          ],
          "summary": "Get agenda templates",
          "description": "Returns a list of agenda templates",
          "operationId": "GetAgendaTemplates",
          "produces": [
            "application/json"
          ],
          "parameters": [
            {
              "in": "query",
              "name": "accountId",
              "x-ms-summary": "Account ID",
              "description": "Specify the unique identifier of the account to filter by",
              "type": "string",
              "format": "uuid"
            },
            {
              "in": "query",
              "name": "agendaTypeId",
              "x-ms-summary": "Agenda Type ID",
              "description": "Specify the unique identifier of the agenda type to filter by",
              "type": "string",
              "format": "uuid"
            }
          ],
          "responses": { ... }
        }
      }
    },
    ...
  }

We can opt-in to add this through the Custom Connectors UI from the Maker portal. It will generate the same syntax behind the scenes:

You can view the updated swagger.json (2-add-x-ms-summary.json) file here.

 

Step 3 – Add Title

To display a user-friendly label for the response properties, we need to update the swagger.json file to include the title attribute on the model definitions:

{
  ...
  "definitions": {
    "AgendaTemplateDto": {
      "type": "object",
      "properties": {
        "id": {
          "format": "uuid",
          "title": "Agenda Template ID",
          "description": "Unique identifier of the agenda template",
          "type": "string"
        },
        "name": {
          "title": "Agenda Template Name",
          "description": "Name of the agenda template",
          "type": "string"
        },
        "agendaTypeName": {
          "title": "Agenda Type Name",
          "description": "Name of the agenda type",
          "type": "string"
        },
        "agendaTypeId": {
          "format": "uuid",
          "title": "Agenda Type ID",
          "description": "Unique identifier of the agenda type",
          "type": "string"
        },
        "accountName": {
          "title": "Account Name",
          "description": "Name of the account",
          "type": "string"
        },
        "accountId": {
          "format": "uuid",
          "title": "Account ID",
          "description": "Unique identifier of the account",
          "type": "string"
        }
      }
    },
    ...
  },
  ...
}

This could have been annotated from the code and automatically included in the generated swagger.json by using the SwaggerSchemaAttribute from the Swashbuckle.AspNetCore.Annotations NuGet package:

[SwaggerSchema("Name of the agenda template", Title = "Agenda Template Name")]
public string Name { get; set; }

Another way is to add it from the Custom Connector UI by clicking on the reference you want to update and selecting a property to edit:

You can view the updated swagger.json (3-add-title.json) file here.

 

Step 4 – Add x-ms-dynamic-values

As the name of this extension suggests, dynamic values allow the users to select a value from a dynamically populated dropdown list to use as input parameters. The values are retrieved by calling an operation that is configured in the OpenAPI definition.

Let’s first look at the operations that will act as the data sources for the two input parameters:

1. Account ID

Please take note of the operationId and the id and name properties in the response model definition. As you can see, this operation returns a payload of type array, an array of AccountDto objects.

{
  "paths": {
    "/api/Accounts": {
      "get": {
        "tags": ["Accounts"],
        "summary": "Get active accounts",
        "description": "Returns a list of accounts",
        "operationId": "GetActiveAccounts",
        "produces": ["application/json"],
        "responses": {
          "200": {
            "description": "Success",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/definitions/AccountDto"
              }
            }
          },
          ...
        }
      }
    },
    ...
  },
  "definitions": {
    "AccountDto": {
      "type": "object",
      "properties": {
        "id": {
          "format": "uuid",
          "title": "Account ID",
          "description": "Unique identifier of the account",
          "type": "string"
        },
        "name": {
          "title": "Account Name",
          "description": "Name of the account",
          "type": "string"
        }
      }
    },
    ...
  },
  ...
}

2. Agenda Type ID

Same as the Account ID, please take note of the operationId and the id and name properties in the response model definition. This operation returns a payload of type array, an array of AgendaTypeDto objects.

{
  "paths": {
    "/api/AgendaTypes": {
      "get": {
        "tags": ["AgendaTypes"],
        "summary": "Get active agenda types",
        "description": "Returns a list of agenda types",
        "operationId": "GetActiveAgendaTypes",
        "produces": ["application/json"],
        "responses": {
          "200": {
            "description": "Success",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/definitions/AgendaTypeDto"
              }
            }
          },
          ...
        }
      }
    },
    ...
  },
  "definitions": {
    "AgendaTypeDto": {
      "type": "object",
      "properties": {
        "id": {
          "format": "uuid",
          "title": "Agenda Type ID",
          "description": "Unique identifier of the agenda type",
          "type": "string"
        },
        "name": {
          "title": "Agenda Type Name",
          "description": "Name of the agenda type",
          "type": "string"
        }
      }
    },
    ...
  },
  ...
}

Now the fun part 😎. We put all of what has been discussed regarding the two input parameters into the GetAgendaTemplates operation definition:

{
  "paths": {
    "/api/AgendaTemplates": {
      "get": {
        "tags": ["AgendaTemplates"],
        "summary": "Get agenda templates",
        "description": "Returns a list of agenda templates",
        "operationId": "GetAgendaTemplates",
        "produces": ["application/json"],
        "parameters": [
          {
            "in": "query",
            "name": "accountId",
            "x-ms-summary": "Account ID",
            "description": "Specify the unique identifier of the account to filter by",
            "type": "string",
            "format": "uuid",
            "x-ms-dynamic-values": {
              "operationId": "GetActiveAccounts",
              "value-path": "id",
              "value-title": "name"
            }
          },
          {
            "in": "query",
            "name": "agendaTypeId",
            "x-ms-summary": "Agenda Type ID",
            "description": "Specify the unique identifier of the agenda type to filter by",
            "type": "string",
            "format": "uuid",
            "x-ms-dynamic-values": {
              "operationId": "GetActiveAgendaTypes",
              "value-path": "id",
              "value-title": "name"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/definitions/AgendaTemplateDto"
              }
            }
          },
          ...
        }
      }
    },
    ...
  },
  ...
}
Note: I have inlined the definition of the input parameters for now but will show you how to make them reusable in the later steps.

We can also define these parameters using x-ms-dynamic-values from the Custom Connectors UI in the Maker portal:

If you have a look at Microsoft’s document on what properties must be set for x-ms-dynamic-values, we have so far covered the operationId, value-title and value-path. For both the GetActiveAccounts and GetActiveAgendaTypes operations, we don’t need to specify the value-collection property because those operations return an array of objects in the response. Just as an example, if I was instead returning an object with an array of objects defined as one of its properties, e.g. OData payload to retrieve multiple Dataverse tables:

{ 
    "@odata.context": "https://yourinstance.crm6.dynamics.com/api/data/v9.1/$metadata#accounts",
    "value": [{…},…] 
} 

In this case, I would need to specify value as the path for the value-collection property.

You can view the updated swagger.json (4-add-x-ms-dynamic-values-basic.json) file here.

Ok, let’s cover the parameters property of x-ms-dynamic-values next!

 

Step 5 – Add x-ms-dynamic-values With Parameters

So far we have an action to retrieve a list of agenda templates. What if we wanted to retrieve a specific agenda template by ID but still want to filter by Account ID and Agenda Type ID?

The good news is that we can apply most of what we have learned so far to build out the OpenAPI definition for this action. We can also demonstrate here that the OpenAPI definition can expand on the Web API controller action and doesn’t necessarily need to be 1-to-1. Let’s look at the following code to see what I’m talking about:

/// <summary>
/// Get agenda template
/// </summary>
/// <remarks>
/// Returns the agenda template by an ID
/// </remarks>
/// <param name="agendaTemplateId">Specify the unique identifier of the agenda template</param>
/// <returns>An agenda template</returns>
[HttpGet("{agendaTemplateId}", Name = "GetAgendaTemplate")]
[ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
public async Task<ActionResult<AgendaTemplateDto>> GetAgendaTemplate(Guid agendaTemplateId)
{
    var query = new GetAgendaTemplateQuery
    {
        AgendaTemplateId = agendaTemplateId,
        IncludeRoleAssignments = true
    };

    return Ok(await _mediator.Send(query));
}

As you can see, this operation accepts only one parameter which is agendaTemplateId. So what about Account ID and Agenda Type ID? Those you can define in the OpenAPI definition as dependency action parameters to call the GetAgendaTemplates operation:

{
  "paths": {
    "/api/AgendaTemplates/{agendaTemplateId}": {
      "get": {
        "tags": ["AgendaTemplates"],
        "summary": "Get agenda template",
        "description": "Returns the agenda template by an ID",
        "operationId": "GetAgendaTemplate",
        "produces": ["application/json"],
        "parameters": [
          {
            "in": "query",
            "name": "accountId",
            "x-ms-summary": "Account ID",
            "description": "Specify the unique identifier of the account to filter by",
            "required": true,
            "type": "string",
            "format": "uuid",
            "x-ms-dynamic-values": {
              "operationId": "GetActiveAccounts",
              "value-path": "id",
              "value-title": "name"
            }
          },
          {
            "in": "query",
            "name": "agendaTypeId",
            "x-ms-summary": "Agenda Type ID",
            "description": "Specify the unique identifier of the agenda type to filter by",
            "required": true,
            "type": "string",
            "format": "uuid",
            "x-ms-dynamic-values": {
              "operationId": "GetActiveAgendaTypes",
              "value-path": "id",
              "value-title": "name"
            }
          },
          {
            "in": "path",
            "name": "agendaTemplateId",
            "x-ms-summary": "Agenda Template ID",
            "description": "Specify the unique identifier of the agenda template",
            "required": true,
            "type": "string",
            "format": "uuid",
            "x-ms-dynamic-values": {
              "operationId": "GetAgendaTemplates",
              "value-path": "id",
              "value-title": "name",
              "parameters": {
                "accountId": {
                  "parameter": "accountId"
                },
                "agendaTypeId": {
                  "parameter": "agendaTypeId"
                }
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "schema": {
              "$ref": "#/definitions/AgendaTemplateDto"
            }
          },
          ...
        }
      },
      ...
    },
    ...
  },
  ...
}

Notice the parameters property on the x-ms-dynamic-values on the agendaTemplateId parameter. It uses the name of the Account ID and Agenda Type ID parameters as references to pass the values required for the GetAgendaTemplates operation.

Note: The "in": "query" property on the Account ID and Agenda Type ID parameters is important here. They must match exactly what the GetAgendaTemplates operation expects for those parameters.  

You can view the updated swagger.json (5-add-x-ms-dynamic-values-parameters.json) file here.

 

Step 6 – Parameter Reusability

You might be asking, “Do we need to define these parameters every time we need to use them in our operations?”. The answer is, “Yes, but we can make them reusable!”. For the following actions, the OpenAPI definition for the parameters will be exactly the same:

In our OpenAPI definition, we can move the parameter definitions into their own section called parameters:

{
  "swagger": "2.0",
  "info": { ... },
  "host": "yourwebapi.azurewebsites.net",
  "schemes": [ ... ],
  "paths": { ... },
  "parameters": {
    "ParamInQueryAccount": {
      "in": "query",
      "name": "accountId",
      "x-ms-summary": "Account ID",
      "description": "Specify the unique identifier of the account to filter by",
      "required": true,
      "type": "string",
      "format": "uuid",
      "x-ms-dynamic-values": {
        "operationId": "GetActiveAccounts",
        "value-path": "id",
        "value-title": "name"
      }
    },
    "ParamInQueryAgendaType": {
      "in": "query",
      "name": "agendaTypeId",
      "x-ms-summary": "Agenda Type ID",
      "description": "Specify the unique identifier of the agenda type to filter by",
      "required": true,
      "type": "string",
      "format": "uuid",
      "x-ms-dynamic-values": {
        "operationId": "GetActiveAgendaTypes",
        "value-path": "id",
        "value-title": "name"
      }
    },
    "ParamInPathAgendaTemplate": {
      "in": "path",
      "name": "agendaTemplateId",
      "x-ms-summary": "Agenda Template ID",
      "description": "Specify the unique identifier of the agenda template",
      "required": true,
      "type": "string",
      "format": "uuid",
      "x-ms-dynamic-values": {
        "operationId": "GetAgendaTemplates",
        "value-path": "id",
        "value-title": "name",
        "parameters": {
          "accountId": {
            "parameter": "accountId"
          },
          "agendaTypeId": {
            "parameter": "agendaTypeId"
          }
        }
      }
    }
  },
  "securityDefinitions": { ... }
}
Note: A naming convention that I found useful is to specify on the parameter name where the parameter will be defined, i.e. path, query, header. You can read about describing the parameters here. 

We can reference these parameters from our operations like the following:

{
  "paths": {
    "/api/AgendaTemplates/{agendaTemplateId}": {
      "get": {
        "tags": ["AgendaTemplates"],
        "summary": "Get agenda template",
        "description": "Returns the agenda template by an ID",
        "operationId": "GetAgendaTemplate",
        "produces": ["application/json"],
        "parameters": [
          { "$ref": "#/parameters/ParamInQueryAccount" },
          { "$ref": "#/parameters/ParamInQueryAgendaType" },
          { "$ref": "#/parameters/ParamInPathAgendaTemplate" }
        ],
        "responses": { ... }
      }
    },
    "/api/AgendaTemplates/{agendaTemplateId}/schema": {
      "get": {
        "tags": ["AgendaTemplates"],
        "summary": "Get agenda template schema",
        "description": "Returns the agenda template schema with agenda roles as properties",
        "operationId": "GetAgendaTemplateSchema",
        "produces": ["application/json"],
        "parameters": [
          { "$ref": "#/parameters/ParamInQueryAccount" },
          { "$ref": "#/parameters/ParamInQueryAgendaType" },
          { "$ref": "#/parameters/ParamInPathAgendaTemplate" }
        ],
        "responses": { ... }
      }
    },
    "/api/AgendaTemplates/{agendaTemplateId}/agendas": {
      "get": {
        "tags": ["AgendaTemplates"],
        "summary": "Get agenda template agendas",
        "description": "Returns a list of agendas using the agenda template",
        "operationId": "GetAgendaTemplateAgendas",
        "produces": ["application/json"],
        "parameters": [
          { "$ref": "#/parameters/ParamInQueryAccount" },
          { "$ref": "#/parameters/ParamInQueryAgendaType" },
          { "$ref": "#/parameters/ParamInPathAgendaTemplate" }
        ],
        "responses": { ... }
      }
    },
    ...
  },
  ...
}

You can view the updated swagger.json (6-parameter-reusability.json) file here.

 

Step 7 – Add x-ms-dynamic-schema

As a finishing touch, we are going to include the dynamic schema (x-ms-dynamic-schema) extension to the steps we have covered so far in this post. To learn more about it, check out Microsoft’s official documentation and my previous post here.

To quickly outline the requirement: an agenda template is used to create repeatable agendas that have the same set of agenda items and attendee roles. Each account may configure different templates for each of the agenda types (e.g. club meetings, contests, executive meetings, etc). Let’s say there is a requirement to return a payload of one of these agendas where each attendee role is defined as a root-level property. Since the schema of the response model has to be dynamic, we need to utilise the dynamic schema extension.

There are two main operations involved here, GetAgendaTemplateSchema and GetAgendaTemplateAgendas:

{
  "paths": {
    "/api/AgendaTemplates/{agendaTemplateId}/schema": {
      "get": {
        "tags": ["AgendaTemplates"],
        "summary": "Get agenda template schema",
        "description": "Returns the agenda template schema with agenda roles as properties",
        "x-ms-visibility": "internal",
        "operationId": "GetAgendaTemplateSchema",
        "produces": ["application/json"],
        "parameters": [
          { "$ref": "#/parameters/ParamInQueryAccount" },
          { "$ref": "#/parameters/ParamInQueryAgendaType" },
          { "$ref": "#/parameters/ParamInPathAgendaTemplate" }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          ...
        }
      }
    },
    "/api/AgendaTemplates/{agendaTemplateId}/agendas": {
      "get": {
        "tags": ["AgendaTemplates"],
        "summary": "Get agenda template agendas",
        "description": "Returns a list of agendas using the agenda template",
        "operationId": "GetAgendaTemplateAgendas",
        "produces": ["application/json"],
        "parameters": [
          { "$ref": "#/parameters/ParamInQueryAccount" },
          { "$ref": "#/parameters/ParamInQueryAgendaType" },
          { "$ref": "#/parameters/ParamInPathAgendaTemplate" }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/definitions/AgendaSchemaDto"
              }
            }
          },
          ...
        }
      }
    },
    ...
  },
  ... 
}

The GetAgendaTemplateSchema operation uses the x-ms-visibility extension with the value internal to hide the action from the user when selecting the action from Power Automate or Power Apps. More about the other visibility options here. The response model for this operation is dynamic therefore we don’t need to return the model definition in the 200 response.

The GetAgendaTemplateAgendas operation returns an array of agenda items, AgendaSchemaDtos, that match the schema returned by the GetAgendaTemplateSchema. This is defined in the definition section of the OpenAPI definition to demonstrate that these definitions can also be reused if necessary:

{
  ...,
  "definitions": {
    "AgendaSchemaDto": {
      "type": "object",
      "x-ms-dynamic-schema": {
        "operationId": "GetAgendaTemplateSchema",
        "parameters": {
          "accountId": {
            "parameter": "accountId"
          },
          "agendaTypeId": {
            "parameter": "agendaTypeId"
          },
          "agendaTemplateId": {
            "parameter": "agendaTemplateId"
          }
        }
      }
    },
    ...
  },
  ...
}

The x-ms-dynamic-schema requires the operationId and parameters to retrieve the schema. The name for the parameter must match the name defined in ParamInQueryAccount, ParamInQueryAgendaType, and ParamInPathAgendaTemplate.

Here is an example payload schema of a club meeting agenda from a Power Automate flow:

You can view the updated swagger.json (7-add-x-ms-dynamic-schema.json) file here.

Hope you enjoyed the post, let me know your thoughts in the comments below or any of my contact channels 😎

Photo by Brook Anderson on Unsplash.