Azure,  Power Apps,  Power Automate

Send Survey Adaptive Card to Microsoft Teams using Power Automate

This is an extension to my last post on creating a survey PCF control using Adaptive Cards Templating. Creating a PCF control is fun, but wouldn’t it be better if we could send the survey directly to the participants?

In the last post, we’ve covered the data structure which makes up the survey and how the survey data JSON gets generated. We also looked at the concept of templating to bind the data JSON and using the client-side Adaptive Cards Templating SDK to transform and render the Adaptive Card. With all this set up, we can now extend the functionality to automate the process of creating the survey requests and send them out to a list of participants. We can do this by utilising Power Automate and Microsoft Teams.

Here is what we are going to cover in this post:

  • Generating a descriptive OpenAPI file using Swashbuckle.AspNetCore to create a custom connector
  • A sneak peak of the .NET Adaptive Cards Templating SDK to generate the Adaptive Card
  • Configuring flows to post the Adaptive Card to participants using a Teams connector

 

Why a Custom Connector?

We will use the following action from the Microsoft Teams connector to send the Adaptive Card message to the participants:

Currently there is a preview version of this action, it allows the Flow makers to create Adaptive Card layouts through an integrated designer on the action itself. (You can play with this by enabling the experimental features on the Power Automate settings)

Unfortunately this preview action does not allow inserting your own template and data JSON as dynamic contents to generate the Adaptive Card. Therefore, to achieve what we need, we will create a custom connector with an action to pass in the template and the data JSON to generate the Adaptive Card using .NET Adaptive Cards Templating SDK.

There are many great articles on how to create custom connectors so I’ll try not to repeat those steps here (unless I find it necessary!). If you haven’t created one before, you can learn more about it here. In this post I’ll concentrate on things that I’ve learnt and found useful with the hopes that it will be helpful to others as well.

 

Create a Web API for the Custom Connector

From make.powerapps.com, custom connectors are stored and created under the section – Data > Custom connectors. There are various options to create new custom connectors here but we will use the “Import an OpenAPI file” option.

So what is an OpenAPI file? As per official definition:

“A document (or set of documents) that defines or describes an API. An OpenAPI definition uses and conforms to the OpenAPI Specification.”

http://spec.openapis.org/oas/v3.0.3#openapi-document

Basically, it allows you to describe the API, including available endpoints and operations, as well as input / output parameters for those endpoints. Because it could be quite intimidating and time consuming to craft one of these by hand, we will use Swashbuckle (Swagger tools for documenting APIs built on ASP.NET Core) to generate the OpenAPI file for our Web API. Due to the naming convention, I’ll use the word Swagger and OpenAPI interchangeably. There is a great article that describes the difference between Swagger and Open API here.

To start, create a new ASP.NET Core web application using Visual Studio. Select .NET Core + ASP.NET Core 3.1. This example will work using .NET Core 2.2 as well but the code will differ slightly:

The API VS project template should give you enough scaffolding to get a simple API running. Add the following NuGet packages to generate the Swagger documentation:

  • Swashbuckle.AspNetCore.Swagger
  • Swashbuckle.AspNetCore.SwaggerGen
  • Swashbuckle.AspNetCore.SwaggerUI
  • Swashbuckle.AspNetCore.Filters
  • Swashbuckle.AspNetCore.Annotations

Also, tick the box for “Include prerelease” and add the following NuGet package for AdaptiveCards.Templating:

  • AdaptiveCards.Templating – 0.1.0-alpha1
Note: The main objective in this section is to generate a descriptive OpenAPI documentation from our code. This gives us a consistent way to create and update the custom connector each time. To demonstrate this, we will be jumping between code, Swagger tool and custom connector designer to get a sense of where each piece of information is used.  

The VS project structure will be like the following:

In the Startup.cs, add the Swagger middleware code:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    ...
     app.UseSwagger(setupAction =>
    {
        setupAction.SerializeAsV2 = true;
        setupAction.PreSerializeFilters.Add((swagger, httpReq) =>
        {
            swagger.Servers = new List<OpenApiServer> 
            { 
                new OpenApiServer { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}" } 
            };
        });
    });

    app.UseSwaggerUI(setupAction =>
    {
        setupAction.SwaggerEndpoint("/swagger/v1/swagger.json", "Adaptive Cards API V1");
        setupAction.RoutePrefix = string.Empty;
    });
    ...
}
  • app.UseSwagger – This exposes the OpenAPI document as a JSON endpoint (i.e. Swagger.json). I find that setting up the swagger.Servers here reduces the manual steps to populate the Host input later when creating the custom connector. For some reason, the custom connector designer doesn’t auto populate this input when using the latest version 3.0 of the OpenAPI specification. This is one of the reasons why I decided to downgrade the OpenApi version from 3.0 to 2.0 by using this line: setupAction.SerializeAsV2 = true; (for now).
  • app.UseSwaggerUI – This enables the Swagger UI tool which produces a user-friendly page that can be used as an interactive tool to describe and test the API.

In the Startup.cs, add the Swagger generator code:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddSwaggerGen(setupAction =>
    {
        setupAction.SwaggerDoc("v1", new OpenApiInfo 
        { 
            Title = "Adaptive Cards API", 
            Version = "v1",
            Description = "Adaptive Cards API to showcase the experiment that I'm concocting",
            Contact = new OpenApiContact
            {
                Name = "Tae Rim Han",
                Email = "taerim.han@test.com",
                Url = new Uri("https://taerimhan.com")
            }
        });

        setupAction.EnableAnnotations();

        setupAction.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
        {
            Type = SecuritySchemeType.OAuth2,
            Flows = new OpenApiOAuthFlows
            {
                AuthorizationCode = new OpenApiOAuthFlow
                {
                    AuthorizationUrl 
                        = new Uri("https://login.windows.net/common/oauth2/authorize", 
                                  UriKind.Absolute),
                    TokenUrl 
                        = new Uri("https://login.windows.net/common/oauth2.token", 
                                   UriKind.Absolute),
                    Scopes = new Dictionary<string, string> {}
                }
            }
        });

        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        setupAction.IncludeXmlComments(xmlPath);
    });
}
  • services.AddSwaggerGen – This registers the Swagger generator. It allows building Swagger document objects directly from your routes, controllers and models.
  • setupAction.SwaggerDoc – This sets up the Swagger document object to expose as Swagger JSON. It allows defining API information such as version, description and author. You can setup multiple documents here.
  • setupAction.EnableAnnotations – Allows annotation made using Swashbuckle.AspNetCore.Annotation to be exposed in the Swagger JSON. We will see an example of this in the DTO class.
  • setupAction.AddSecurityDefinition – Defining the security definition here reduces the manual steps later when setting up the security information for the custom connector. This is also helpful if your API is secured at the code level. The Authorize button on the Swagger UI page will let you configure the authorization details for your API to secure and test the API from the page.
  • setupAction.IncludeXmlComments – Enables human friendly descriptions made in code comments to be incorporated into the Swagger JSON (e.g. /// <Summary />). Make sure to turn on the XML documentation file from the VS project properties: Build > Output > XML documentation file.

 

In the AdaptiveCardsController.cs:

[ApiController]
[Produces("application/json")]
[Route("api/[controller]")]
public class AdaptiveCardsController : ControllerBase
{
    /// <summary>
    /// Create Adaptive Card JSON
    /// </summary>
    /// <remarks>
    ///     POST api/adaptivecards
    ///     {
    ///         "templateJson": "{\\"type\\": \\"AdaptiveCard\\",\\"version\\": \\"1.0\\",\\"body\\": [{\\"type\\": \\"TextBlock\\",\\"text\\": \\"Hello {name}\\"}]}",
    ///         "dataJson": "{\\"name\\": \\"Power Platform People!\\"}"
    ///     }
    /// </remarks>
    /// <param name="adaptiveCard">Adaptive Card DTO With Template and Data JSON</param>
    /// <returns>Adaptive Card JSON</returns>
    [HttpPost(Name = "CreateAdaptiveCardJson")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesDefaultResponseType()]
    public ActionResult<string> CreateAdaptiveCardJson([FromBody, Required] CreateAdaptiveCardDto adaptiveCard)
    {
        var transformer = new AdaptiveTransformer();
        var cardJson = transformer.Transform(adaptiveCard.TemplateJson, adaptiveCard.DataJson);  
        return cardJson;
    }
}
  • Human friendly comments such as /// <summary /> and /// <remarks /> will be used to define the summary and the description of the API action. The Operation ID is annotated using [HttpPost(Name = "CreateAdaptiveCardJson")]

    Describing the API with a usage is useful for API consumers and for testing in the Swagger UI:
  • Defining the correct response code and type is important to validate and form a contract of what is being returned at each endpoint. This can be annotated using the [ProducesResponseType] and [ProducesDefaultResponseType]. I’ve once made the mistake of describing a different Response Type to what was returned at an endpoint and it was only when executing the custom connector that I realised it wasn’t correct!
  • transformer.Transform – Generating the Adaptive Card JSON from the template and the data JSON is pretty simple when using the transform method of the templating engine. Please note that this NuGet package is still in alpha version. Currently there is a bug in the .NET SDK where it incorrectly calculates the results of $when and $if scopes when used within the repeating method. Hence my JSON template from my previous post won’t work here, sadly. There is an issue raised in github regarding this.
  • [FromBody, Required] – I want to point out the Required attribute on the parameter: From my experimentation, if you don’t set the Required schema on the payload body, the custom connector will ignore the Required schema set on the properties:

 

In the CreateAdaptiveCardDto.cs:

[SwaggerSchema(Required = new[] { "templateJson", "dataJson" })]
public class CreateAdaptiveCardDto
{
    [Required]
    [SwaggerSchema("Adaptive Card Template JSON")]
    public string TemplateJson { get; set; }
        
    [Required]
    [SwaggerSchema("Adaptive Card Data JSON")]
    public string DataJson { get; set; }
}
  • [SwaggerSchema(Required = new[] { "templateJson", "dataJson" })] – This sets the schema for the defined properties as required.
  • [SwaggerSchema("Adaptive Card Template JSON")] – Describes the property
The Title should be populated to get a more user friendly parameter name on the connector action. For example, not “templateJson” but “Template JSON”.

 

This is a brief outline of the steps I took to host and secure the API before uploading the OpenAPI file to create the custom connector:

  1. Published the Wep API to Azure App Service using Visual Studio Publish functionality. The Publish Profile can be either configured through VS or exported from Azure and imported into the VS project.
  2. Enabled Authentication / Authorization on Azure App Service. Selected Azure Active Directory as authentication provider and then created an Azure AD app registration through the express configuration.
  3. Tested the Web API URL to verify that it asked for authentication. Made sure that all requests are authenticated.
  4. Created an Azure AD app registration for the custom connector. Granted permission to the Web API and generated client secret and stored it safely, as this will only be shown once, along with the client ID of the app registration. These are used in the security section of the custom connector designer.
  5. Downloaded the OpenAPI file (Swagger.json) by navigating to the Swagger UI page and clicking on the link to the Swagger JSON endpoint.
  6. Used the downloaded OpenAPI file to create the custom connector in make.powerapps.com. From here, the only inputs you will manually populate will be at the Security screen to define the Client Id, Client Secret and Resource URL. Optionally, set the user friendly Title for the properties at the Definition screen.
  7. At the Security screen, make sure to copy the Redirect URL and configure it on the custom connector Azure AD app registration.

I deliberately didn’t go into detail to perform the above steps as Microsoft Docs has two great articles on how to do this. You can find them here:

https://docs.microsoft.com/en-us/connectors/custom-connectors/create-web-api-connector
https://docs.microsoft.com/en-us/connectors/custom-connectors/create-custom-connector-aad-protected-azure-functions

 

Create Flow to Post a Survey Message to Microsoft Teams

Now let’s put it all together and consume the custom connector from Power Automate. I have currently set up two flows to automate the sending of the survey request to the participants:

  1. On-demand flow on the survey record to update the Send Date:
  2. Solution-enabled flow to detect an update on a survey record which filters on the Send Date attribute only:

The reason for having two separate flows is because the solution-enabled flows cannot be triggered on-demand. Also, the connector for the Common Data Service (current environment), which provides more complex actions on CDS, is not exposed on flows that are created outside the solution.

I’ve scoped the flow into three stages:

First stage, we collect any previous requests sent out for this survey so we don’t re-send the requests to the participants who have already received it. We do this by collecting the participant id for the sent requests in an array variable.

Second stage, we generate the Adaptive Card for the survey using the custom connector we’ve built by passing in the template and the data JSON we retrieve from the Survey and the Adaptive Card Template records.

Note: When searching for your custom connector, it will appear under Custom and you will find the list of actions provided by the connector under Actions:

Last stage, we iterate over all the survey participants and check if they’ve already received the request and if not, create a survey request and send the survey Adaptive Card message to the participant through Microsoft Teams. The Teams post action will wait for the user response and save the submitted result back to the survey request record.

The string output from the Create Adaptive Card JSON action will be inserted into the Teams Message input:

The Flow execution will wait for the user to submit the survey from Teams. The submitted data, which will be in the form of JSON, will be stored against the survey record as Response JSON. On the record update, the plugin will execute to create answer records from the submitted data.

Note: There was a side effect of using dynamic content for the Teams message where it couldn't recognize any dynamic content to utilise from the Teams Post action. So I had to manually insert the expression into the Response Json input:  outputs('Post_a_survey_Adaptive_Card_to_a_Teams_participant_user_and_wait_for_a_response')?['body/data']

 

Submit the Survey… but this time from Microsoft Teams!

Set up of the survey and the questions were covered in the previous post so let’s execute the Flow to generate the Adaptive Card JSON:

As one of the survey participants, I received a message from Flow in Microsoft Teams to complete the survey:

Once I click on the Submit Survey button, the Adaptive Card message will be replaced with an Update Message defined in the Teams Post action:

The Flow will continue to execute the next action:

Next action will update the survey request record with the survey data submitted from Teams:

This update will trigger a plugin which will parse the Response JSON to create the answer records in Power Apps.

OK, we have now covered how to create a custom connector, configure the Flow and use Microsoft Teams to submit data back to Power Apps! If you have any questions or ideas you want to discuss, please let me know, otherwise I hope you have enjoyed the post 🙂