Azure Serverless Compute Integration with Power Platform
Integrating Azure serverless compute resources—specifically Azure Functions and Azure Logic Apps—into Microsoft Power Platform solutions is a core architectural pattern for enterprise-grade extensibility. This integration allows developers to offload long-running, resource-intensive, or complex business logic from the Dataverse event pipeline, connect to external systems, and build highly scalable, event-driven architectures.
This chapter provides an exhaustive, production-grade guide to designing, implementing, securing, and deploying these integrations, fully aligned with the PL-400: Microsoft Power Platform Developer exam objectives.
1. Conceptual Foundation
Enterprise integration between the Power Platform and Azure Serverless compute relies on an event-driven, decoupled architecture. Rather than executing all business logic within the synchronous execution thread of Microsoft Dataverse, developers offload processing to Azure Functions and Azure Logic Apps. This preserves Dataverse platform resources, avoids sandbox execution timeouts, and enables integration with external systems.
The Dataverse Event Framework and Extensibility
Dataverse operates on an event-driven database engine. Every data operation (Create, Update, Delete, Retrieve, Associate, etc.) triggers a multi-stage execution pipeline. While synchronous plug-ins execute within this pipeline, they are subject to strict resource constraints, including a hard 2-minute execution timeout and sandbox isolation restrictions.
To extend this framework beyond these boundaries, Dataverse provides native integration points to propagate events to Azure:
+---------------------------------------------------------------------------------+
| DATAVERSE |
| |
| +------------------+ +-------------------+ +------------------------+ |
| | Data Operation | --> | Event Pipeline | --> | Out-of-the-Box Plug-in | |
| +------------------+ +-------------------+ +------------------------+ |
+---------------------------------------------------------------------------------+
|
+----------------------------+
| (RemoteExecutionContext)
v
+---------------------------------------------------------------------------------+
| AZURE GATEWAY |
| |
| +---------------------------+ +-------------------------+ |
| | Azure Webhook | | Azure Service Bus | |
| +---------------------------+ +-------------------------+ |
+---------------------------------------------------------------------------------+
| |
v v
+---------------------------------------------------------------------------------+
| AZURE SERVERLESS |
| |
| +---------------------------+ +-------------------------+ |
| | Azure Function | | Azure Logic App | |
| +---------------------------+ +-------------------------+ |
+---------------------------------------------------------------------------------+
- Webhooks: Dataverse can post HTTP POST payloads directly to an external HTTP endpoint, such as an HTTP-triggered Azure Function or an Azure Logic App Request trigger.
- Service Bus Integration: Dataverse can publish execution contexts directly to Azure Service Bus queues, topics, or relays.
- Event Hubs: For high-throughput telemetry and event streaming, Dataverse can stream event data directly to Azure Event Hubs.
RemoteExecutionContext: The Integration Contract
When an event is propagated from Dataverse to an Azure service, the payload is serialized as a RemoteExecutionContext object. This class implements IPluginExecutionContext and contains the complete state of the operation that triggered the event.
Key properties of the RemoteExecutionContext include:
- InputParameters: A collection of the input arguments for the message request (e.g., the
Targetentity being created or updated). - OutputParameters: A collection of the output arguments from the message response (available only in post-operation stages).
- PreEntityImages / PostEntityImages: Snapshots of the primary entity's attributes before and after the core database operation.
- PrimaryEntityName: The logical name of the table (e.g.,
account,contact). - MessageName: The SDK message name (e.g.,
Create,Update,Delete,Win,Lose). - Stage: The integer value representing the pipeline stage (e.g.,
10for Pre-validation,20for Pre-operation,40for Post-operation). - InitiatingUserId: The unique identifier of the system user who started the operation.
- CorrelationId: A unique identifier used to track the execution chain across multiple plug-ins, workflows, and external Azure services.
Serialization Formats
Dataverse supports three serialization formats for the RemoteExecutionContext when posting to Azure Service Bus or Event Hubs:
- JSON: The default and recommended format for cross-platform interoperability. It allows non-.NET consumers (such as Node.js or Python Azure Functions) to easily parse the payload.
- XML: A structured text format, useful for legacy systems or XML-based enterprise service buses.
- Binary: A proprietary .NET serialization format. It requires the consumer to reference the Dataverse SDK assemblies (
Microsoft.Xrm.Sdk.dll) to deserialize the payload back into aRemoteExecutionContextobject.
Note: Webhooks registered via the Plugin Registration Tool always transmit the payload as a serialized JSON string representing the RemoteExecutionContext.
Execution Boundaries and Transactional Integrity
When designing integrations, defining execution boundaries is critical to maintaining data consistency:
- Synchronous Webhooks/Service Bus: If a webhook or Service Bus endpoint is registered as a synchronous step (Pre-operation or Post-operation), the Dataverse transaction waits for the external service to respond. If the external service returns an HTTP failure code or a fault, the entire Dataverse transaction rolls back. Warning: Synchronous execution blocks the user interface and consumes Dataverse worker threads. It should be used sparingly and must complete well within the 2-minute timeout.
- Asynchronous Webhooks/Service Bus: The event is queued in the Dataverse
AsyncOperationtable. The asynchronous service processes the queue and posts the payload to the Azure endpoint. The Dataverse transaction completes immediately without waiting for the Azure service. This is the preferred pattern for non-blocking, high-scale integrations.
Managed Identities and Federated Credentials
Securing cross-service communication without managing secrets is a modern security requirement. Power Platform supports User-Assigned Managed Identities (UAMI) for Dataverse plug-ins and service endpoints.
This integration relies on Federated Identity Credentials (FIC). Dataverse acts as an OpenID Connect (OIDC) identity provider. When a plug-in or service endpoint executes, it requests an Azure AD (Entra ID) token by presenting a federated token issued by Dataverse. Entra ID validates the trust relationship, verifies the federated credential, and issues an access token for the target Azure resource (e.g., Azure Key Vault, Azure Service Bus, or Azure Functions). This eliminates the need to store client secrets or certificates in Dataverse configurations.
2. Architecture & Decision Matrix
When extending the Power Platform, architects must choose the correct tool for the job. The table below compares the five primary implementation approaches:
| Feature / Dimension | Power Automate Cloud Flows | Azure Logic Apps (Standard) | Azure Functions (Consumption) | Dataverse Plug-ins (C# Sandbox) | Power Apps Component Framework (PCF) |
|---|---|---|---|---|---|
| Complexity | Low (No-code / Low-code) | Medium (Visual designer + JSON) | High (Pro-code C# / TypeScript) | High (Pro-code C#) | High (Pro-code TypeScript / React) |
| Scalability | Moderate (Subject to API limits) | High (Enterprise integration engine) | Extremely High (Serverless scale-out) | Moderate (Bound to Dataverse sandbox) | Client-side (Runs in user's browser) |
| Execution Context | Asynchronous (Sync via trigger action is limited) | Asynchronous (Supports synchronous request/response) | Asynchronous & Synchronous (via Webhooks) | Synchronous & Asynchronous (In-pipeline) | Client-side UI execution |
| Offline Support | No | No | No | Yes (Offline-enabled mobile clients) | Yes (Within canvas app offline context) |
| Licensing | Power Apps/Power Automate per-user/per-flow | Azure Subscription (Pay-per-execution / App Service Plan) | Azure Subscription (Consumption / Premium / Flex) | Included in Dataverse capacity | Included in Power Apps licensing |
| PL-400 Exam Relevance | High (Flow triggers, actions, solutions) | High (Integration boundaries, triggers) | Critical (Webhooks, custom connectors, bindings) | Critical (Event pipeline, sandbox, SDK) | Critical (Manifest, lifecycle, Web API) |
Architectural Decision Trees
Scenario A: High-Throughput Batch Processing
If you need to process 50,000 records modified in Dataverse every night and sync them to an on-premises ERP:
- Do NOT use Power Automate: You will hit API request limits and run execution timeouts.
- Do NOT use synchronous Plug-ins: You will block the database and hit the 2-minute timeout.
- USE Azure Functions (Premium/Flex) triggered by Azure Service Bus: Dataverse publishes lightweight change events asynchronously to a Service Bus queue. The Azure Function scales out to process the queue in parallel, utilizing batching (
ExecuteMultipleRequest) to write back to Dataverse.
Scenario B: Real-Time External API Validation
If you must validate a credit card number with a third-party gateway before a Dataverse record can be saved:
- USE a Synchronous C# Plug-in calling an Azure Function: Register the plug-in on the
Pre-Operationstage. The plug-in calls the Azure Function via an HTTP client. If the Function returns an invalid status, the plug-in throws anInvalidPluginExecutionException, rolling back the transaction and displaying a clean error message to the user.
Scenario C: Complex Multi-System Orchestration
If a new customer onboarding requires creating records in Dataverse, provisioning an Azure AD account, creating a folder in SharePoint, and sending a message to a Teams channel:
- USE Azure Logic Apps (Standard): Logic Apps provides out-of-the-box enterprise connectors for SharePoint, Teams, and Entra ID, combined with a robust visual orchestrator that handles state, retries, and long-running workflows far better than custom code in an Azure Function.
3. Step-by-Step Implementation Guide
This guide walks through implementing a secure, event-driven integration: Dataverse triggers an asynchronous Webhook on record creation, which calls an Azure Function secured by Microsoft Entra ID. The Azure Function uses a User-Assigned Managed Identity to authenticate back to Dataverse and update the record.
+-----------------+ +------------------+ +-----------------+
| DATAVERSE | | AZURE WEBHOOK | | AZURE FUNCTION |
| | | | | |
| Record Created | --------------> | Triggers HTTP | --------------> | Parses Payload |
| | (Secure POST) | | (OIDC Token) | |
+-----------------+ +------------------+ +-----------------+
^ |
| |
| v
| +-----------------+
| | Managed Identity|
+----------------------------------------------------------------| |
(Updates Dataverse Record) | Acquires Token |
+-----------------+
Step 1: Provision Azure Resources
We will provision an Azure Function App, a User-Assigned Managed Identity (UAMI), and an Azure Key Vault using the Azure CLI.
Open your terminal and execute the following commands:
# 1. Define variables
$resourceGroup = "rg-powerplatform-integration-prod"
$location = "eastus"
$storageAccount = "stppintegrationprod"
$functionApp = "func-dataverse-processor-prod"
$managedIdentity = "id-dataverse-processor-prod"
$keyVault = "kv-pp-integration-prod"
# 2. Create Resource Group
az group create --name $resourceGroup --location $location
# 3. Create Storage Account for the Function App
az storage account create --name $storageAccount --location $location --resource-group $resourceGroup --sku Standard_LRS
# 4. Create User-Assigned Managed Identity
$identityJson = az identity create --name $managedIdentity --resource-group $resourceGroup --location $location | ConvertFrom-Json
$principalId = $identityJson.principalId
$clientId = $identityJson.clientId
$identityId = $identityJson.id
# 5. Create Azure Key Vault
az keyvault create --name $keyVault --resource-group $resourceGroup --location $location --enable-rbac-authorization true
# 6. Create Function App (Isolated Worker Model, .NET 8)
az functionapp create --name $functionApp --resource-group $resourceGroup --consumption-plan-location $location --runtime dotnet-isolated --runtime-version 8 --functions-version 4 --storage-account $storageAccount --assign-identity $identityId
Step 2: Configure Federated Identity Credentials (FIC)
To allow the Azure Function to authenticate back to Dataverse without secrets, we must establish a federated trust between our User-Assigned Managed Identity and the Dataverse environment.
- Open the Power Platform Admin Center, navigate to your environment, and copy the Environment URL (e.g.,
https://org12345.crm.dynamics.com). - Get the Dataverse Organization ID from the Developer Resources page in the Power Apps Maker Portal.
- Use the Power Platform CLI (
pac) to link the managed identity to your Dataverse environment:
# Authenticate to your Dataverse environment
pac auth create --url https://org12345.crm.dynamics.com
# Create and link the managed identity to Dataverse
# This command registers the UAMI in Dataverse and configures the Federated Identity Credential in Entra ID
pac managed-identity create --application-id $clientId --tenant-id "YOUR_TENANT_ID" --component-type "PluginAssembly" --component-id "00000000-0000-0000-0000-000000000000"
Note: The --component-id can be set to a dummy GUID if you are linking the identity for general Webhook/API access rather than a specific C# plug-in assembly.
Step 3: Create the Dataverse Application User
For the Azure Function to perform operations in Dataverse, it must be registered as an Application User and assigned a security role.
- Go to the Power Platform Admin Center (
admin.powerplatform.microsoft.com). - Select your environment, then go to Settings > Users + permissions > Application users.
- Click New app user.
- Click Add an app, search for the User-Assigned Managed Identity by its Client ID or Name, and select it.
- Select the Business Unit.
- Click Edit security roles, select a custom security role that grants the necessary privileges (e.g., Read/Write on the target table), and click Save.
Step 4: Develop the Azure Function
Create a new .NET 8 Isolated Worker Azure Function project. We will use the Microsoft.PowerPlatform.Dataverse.Client NuGet package to connect to Dataverse using the managed identity.
Add the required NuGet packages to your project:
dotnet add package Microsoft.Azure.Functions.Worker
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Http
dotnet add package Microsoft.PowerPlatform.Dataverse.Client --version 1.2.10
dotnet add package Azure.Identity
Implement the HTTP-triggered function to parse the RemoteExecutionContext and update Dataverse. The complete, compilable code is provided in Section 4.
Step 5: Register the Webhook in Dataverse
We will use the Plugin Registration Tool (PRT) to register our Azure Function as a Webhook in Dataverse.
- Open the Plugin Registration Tool (run
pac tool prtfrom your terminal). - Click Create New Connection and log into your Dataverse environment.
- Click Register > Register New WebHook.
- Configure the Webhook properties:
- Name:
WebhookToAzureFunction - Endpoint URL: Enter your Azure Function HTTP trigger URL (e.g.,
https://func-dataverse-processor-prod.azurewebsites.net/api/ProcessDataverseEvent). - Authentication: Select WebhookKey.
- Value: Enter the Function Key (
codeparameter) obtained from the Azure Portal for your function.
- Name:
- Click Save.
+-------------------------------------------------------------------+
| Register New WebHook |
+-------------------------------------------------------------------+
| Name: WebhookToAzureFunction |
| Endpoint URL: https://func-dataverse-processor-prod.azure... |
| Authentication: [ WebhookKey ] |
| Value: [ dGhpcyBpcyBhIHNlY3JldCBmdW5jdGlvbiBrZXkgZm9yIGF6dXJl... ]|
+-------------------------------------------------------------------+
| [ Save ] [ Cancel ] |
+-------------------------------------------------------------------+
Step 6: Register the Step on the Webhook
Now, register a step to trigger the Webhook when a record is created.
- Right-click the newly registered Webhook in the PRT and select Register New Step.
- Configure the Step properties:
- Message:
Create - Primary Entity:
account - Event Pipeline Stage of Execution:
PostOperation - Execution Mode:
Asynchronous - Deployment:
Server
- Message:
- Click Register New Step.
4. Complete Code Reference
4.1. Azure Function (Isolated Worker Model, .NET 8)
This production-grade C# Azure Function is designed to receive a Dataverse Webhook payload, deserialize it into a RemoteExecutionContext, extract the target entity, and use a User-Assigned Managed Identity to write back to Dataverse.
//-----------------------------------------------------------------------
// <copyright file="ProcessDataverseEvent.cs" company="Enterprise Integration">
// Copyright (c) Enterprise Integration. All rights reserved.
// </copyright>
// <summary>
// Azure Function to process Dataverse Webhook events and update records
// using User-Assigned Managed Identity authentication.
// </summary>
//-----------------------------------------------------------------------
using System;
using System.IO;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using Azure.Identity;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
namespace Enterprise.Integration.Functions
{
/// <summary>
/// Represents the Azure Function that processes Dataverse Webhook events.
/// </summary>
public class ProcessDataverseEvent
{
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ProcessDataverseEvent"/> class.
/// </summary>
/// <param name="loggerFactory">The logger factory used to create loggers.</param>
public ProcessDataverseEvent(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<ProcessDataverseEvent>();
}
/// <summary>
/// Runs the HTTP-triggered function to process a Dataverse Webhook payload.
/// </summary>
/// <param name="req">The HTTP request envelope.</param>
/// <returns>An HTTP response indicating success or failure.</returns>
[Function("ProcessDataverseEvent")]
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
_logger.LogInformation("C# HTTP trigger function processed a request from Dataverse Webhook.");
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
if (string.IsNullOrEmpty(requestBody))
{
_logger.LogError("Empty request body received.");
var badResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badResponse.WriteStringAsync("Request body cannot be empty.");
return badResponse;
}
try
{
// Deserialize the payload into RemoteExecutionContext
// Note: RemoteExecutionContext is part of Microsoft.Xrm.Sdk namespace
RemoteExecutionContext context = JsonSerializer.Deserialize<RemoteExecutionContext>(requestBody, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (context == null)
{
_logger.LogError("Failed to deserialize RemoteExecutionContext.");
var badResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badResponse.WriteStringAsync("Invalid RemoteExecutionContext payload.");
return badResponse;
}
_logger.LogInformation($"Received event for Message: {context.MessageName}, Entity: {context.PrimaryEntityName}, CorrelationId: {context.CorrelationId}");
// Extract the Target entity from InputParameters
if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity targetEntity)
{
_logger.LogInformation($"Processing entity with ID: {targetEntity.Id}");
// Initialize Dataverse ServiceClient using User-Assigned Managed Identity
string dataverseUrl = Environment.GetEnvironmentVariable("DATAVERSE_INSTANCE_URL");
string managedIdentityClientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");
if (string.IsNullOrEmpty(dataverseUrl))
{
throw new InvalidOperationException("DATAVERSE_INSTANCE_URL environment variable is not configured.");
}
_logger.LogInformation($"Connecting to Dataverse at {dataverseUrl} using Managed Identity Client ID: {managedIdentityClientId}");
// Configure TokenCredential using DefaultAzureCredentialOptions to target the User-Assigned Managed Identity
var credentialOptions = new DefaultAzureCredentialOptions();
if (!string.IsNullOrEmpty(managedIdentityClientId))
{
credentialOptions.ManagedIdentityClientId = managedIdentityClientId;
}
var credential = new DefaultAzureCredential(credentialOptions);
// Create the ServiceClient instance
using (var serviceClient = new ServiceClient(new Uri(dataverseUrl), async (uri) =>
{
var tokenRequestContext = new Azure.Core.TokenRequestContext(new[] { $"{dataverseUrl}/.default" });
var tokenResult = await credential.GetTokenAsync(tokenRequestContext);
return tokenResult.Token;
}))
{
if (!serviceClient.IsReady)
{
throw new Exception($"ServiceClient is not ready. Error: {serviceClient.LastError}", serviceClient.LastException);
}
// Perform business logic: Update a custom attribute on the triggering record
Entity updateEntity = new Entity(context.PrimaryEntityName, targetEntity.Id);
updateEntity["description"] = $"Processed by Azure Function on {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC. Correlation ID: {context.CorrelationId}";
_logger.LogInformation($"Updating record {targetEntity.Id} in Dataverse...");
await serviceClient.UpdateAsync(updateEntity);
_logger.LogInformation("Record updated successfully.");
}
}
else
{
_logger.LogWarning("Target entity not found in InputParameters.");
}
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteStringAsync("Event processed successfully.");
return response;
}
catch (Exception ex)
{
_logger.LogError($"Error processing Dataverse event: {ex.Message}. StackTrace: {ex.StackTrace}");
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteStringAsync($"Internal Server Error: {ex.Message}");
return errorResponse;
}
}
}
}
4.2. Infrastructure as Code: Bicep Template
This Bicep template deploys the Azure Function App, configures the User-Assigned Managed Identity, sets up the Key Vault, and configures the necessary application settings.
//-----------------------------------------------------------------------
// <auto-generated>
// This Bicep file deploys the serverless integration infrastructure.
// </auto-generated>
//-----------------------------------------------------------------------
@description('The location where resources will be deployed.')
param location string = resourceGroup().location
@description('The name of the Azure Function App.')
param functionAppName string = 'func-dataverse-processor-prod'
@description('The name of the User-Assigned Managed Identity.')
param managedIdentityName string = 'id-dataverse-processor-prod'
@description('The name of the Azure Key Vault.')
param keyVaultName string = 'kv-pp-integration-prod'
@description('The URL of the Dataverse instance.')
param dataverseInstanceUrl string = 'https://org12345.crm.dynamics.com'
// 1. Deploy User-Assigned Managed Identity
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: managedIdentityName
location: location
}
// 2. Deploy Storage Account for Function App
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'stppintegrationprod'
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
}
}
// 3. Deploy Hosting Plan (Consumption Plan)
resource hostingPlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: 'plan-dataverse-processor-prod'
location: location
sku: {
name: 'Y1'
tier: 'Dynamic'
}
properties: {}
}
// 4. Deploy Key Vault
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: keyVaultName
location: location
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: subscription().tenantId
enableRbacAuthorization: true
enabledForDeployment: false
enabledForDiskEncryption: false
enabledForTemplateDeployment: false
}
}
// 5. Deploy Function App
resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
name: functionAppName
location: location
kind: 'functionapp'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'`${managedIdentity.id}`': {}
}
}
properties: {
serverFarmId: hostingPlan.id
siteConfig: {
appSettings: [
{
name: 'AzureWebJobsStorage'
value: 'DefaultEndpointsProtocol=https;AccountName=`${storageAccount.name}`;AccountKey=`${storageAccount.listKeys().keys[0].value}`;EndpointSuffix=`${environment().suffixes.storage}`'
}
{
name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
value: 'DefaultEndpointsProtocol=https;AccountName=`${storageAccount.name}`;AccountKey=`${storageAccount.listKeys().keys[0].value}`;EndpointSuffix=`${environment().suffixes.storage}`'
}
{
name: 'WEBSITE_CONTENTSHARE'
value: toLower(functionAppName)
}
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~4'
}
{
name: 'FUNCTIONS_WORKER_RUNTIME'
value: 'dotnet-isolated'
}
{
name: 'DATAVERSE_INSTANCE_URL'
value: dataverseInstanceUrl
}
{
name: 'AZURE_CLIENT_ID'
value: managedIdentity.properties.clientId
}
]
ftpsState: 'Disabled'
minTlsVersion: '1.2'
}
httpsOnly: true
}
}
// Assign Key Vault Secrets User role to the Managed Identity
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(keyVault.id, managedIdentity.id, '46330157-b7c1-4b1e-951d-ca456a260f02') // Key Vault Secrets User Role ID
scope: keyVault
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '46330157-b7c1-4b1e-951d-ca456a260f02')
principalId: managedIdentity.properties.principalId
principalType: 'ServicePrincipal'
}
}
4.3. Custom Connector OpenAPI Definition
This OpenAPI 2.0 (Swagger) JSON file defines the Custom Connector used by Power Apps and Power Automate to securely call our Azure Function.
{
"swagger": "2.0",
"info": {
"title": "DataverseEventProcessor",
"description": "Custom Connector to invoke the Azure Function Event Processor.",
"version": "1.0.0"
},
"host": "func-dataverse-processor-prod.azurewebsites.net",
"basePath": "/api",
"schemes": [
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/ProcessDataverseEvent": {
"post": {
"summary": "Trigger Event Processing",
"description": "Sends a Dataverse execution context payload to the Azure Function for processing.",
"operationId": "ProcessEvent",
"parameters": [
{
"name": "code",
"in": "query",
"required": true,
"type": "string",
"description": "The Azure Function authorization key."
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/RemoteExecutionContext"
}
}
],
"responses": {
"200": {
"description": "Success",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request"
},
"500": {
"description": "Internal Server Error"
}
}
}
}
},
"definitions": {
"RemoteExecutionContext": {
"type": "object",
"properties": {
"BusinessUnitId": {
"type": "string",
"format": "uuid"
},
"CorrelationId": {
"type": "string",
"format": "uuid"
},
"Depth": {
"type": "integer"
},
"InitiatingUserId": {
"type": "string",
"format": "uuid"
},
"MessageName": {
"type": "string"
},
"Mode": {
"type": "integer"
},
"PrimaryEntityName": {
"type": "string"
},
"PrimaryEntityId": {
"type": "string",
"format": "uuid"
},
"Stage": {
"type": "integer"
},
"InputParameters": {
"type": "array",
"items": {
"$ref": "#/definitions/KeyValuePair"
}
}
}
},
"KeyValuePair": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"value": {
"type": "object"
}
}
}
},
"securityDefinitions": {
"APIKey": {
"type": "apiKey",
"in": "header",
"name": "x-functions-key"
}
},
"security": [
{
"APIKey": []
}
]
}
4.4. ALM Deployment Settings File (deployment-settings.json)
This file is used by the Power Platform Build Tools to automatically map connection references and environment variables during automated CI/CD pipeline deployments.
{
"EnvironmentVariables": [
{
"SchemaName": "contoso_DataverseInstanceUrl",
"Value": "https://org12345.crm.dynamics.com"
},
{
"SchemaName": "contoso_AzureFunctionUrl",
"Value": "https://func-dataverse-processor-prod.azurewebsites.net/api/ProcessDataverseEvent"
}
],
"ConnectionReferences": [
{
"ConnectionReferenceLogicalName": "contoso_sharedcommondatashift_89a12",
"ConnectionId": "00000000-0000-0000-0000-000000000000",
"ConnectorId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps"
}
]
}
5. Configuration & Environment Setup
Environment Variables in Power Platform
Environment variables in Power Platform allow developers to store configuration keys and values separately from the solution components. This is critical for ALM, as it allows a solution to point to development resources in a Dev environment and production resources in a Prod environment without modifying the underlying apps or flows.
Environment Variable Schema Definitions
Dataverse stores environment variables in two tables:
- Environment Variable Definition (
environmentvariabledefinition): Stores the schema name, display name, description, and default value. - Environment Variable Value (
environmentvariablevalue): Stores the environment-specific value. This record is excluded from managed solutions to allow target environments to maintain their own values.
Referencing Azure Key Vault Secrets
To secure sensitive configuration data (such as API keys or connection strings), Power Platform supports environment variables of type Secret that reference Azure Key Vault.
+---------------------------------------------------------------------------------+
| DATAVERSE |
| |
| +----------------------------------+ +----------------------------------+ |
| | Environment Variable Definition | --> | Environment Variable Value | |
| | (Type: Secret) | | (Points to Key Vault Secret) | |
| +----------------------------------+ +----------------------------------+ |
+---------------------------------------------------------------------------------+
|
v
+---------------------------------------------------------------------------------+
| AZURE KEY VAULT |
| |
| +-----------------------------+ |
| | Secure Secret | |
| +-----------------------------+ |
+---------------------------------------------------------------------------------+
To configure this:
- Register the
Microsoft.PowerPlatformresource provider in your Azure subscription. - Grant the Key Vault Secrets User role to the Dataverse Service Principal (App ID:
00000007-0000-0000-c000-000000000000) on your Key Vault. - Create an environment variable in your solution:
- Data Type:
Secret - Secret Store:
Azure Key Vault - Subscription ID: The GUID of your Azure subscription.
- Resource Group Name: The name of your resource group.
- Key Vault Name: The name of your Key Vault.
- Secret Name: The name of the secret in Key Vault.
- Data Type:
Solution Publisher Prefix Rules
When creating environment variables, custom connectors, or tables, always use the unique prefix assigned to your solution publisher (e.g., contoso_). This prevents naming collisions during solution import and ensures clean dependency tracking.
6. Security & Permission Matrix
Securing cross-service communication requires configuring permissions across both Azure and the Power Platform. The table below outlines the required security configuration:
| Principal | Permission / Role | Scope | Reason |
|---|---|---|---|
| User-Assigned Managed Identity | Key Vault Secrets User | Azure Key Vault | Allows the Azure Function to retrieve secrets at runtime. |
| User-Assigned Managed Identity | Reader | Azure Resource Group | Allows the identity to discover resources within the group. |
| Dataverse Service Principal | Key Vault Secrets User | Azure Key Vault | Allows Dataverse to retrieve secrets referenced by Secret Environment Variables. |
| Dataverse Application User | Custom Security Role (e.g., Account Writer) | Dataverse Business Unit | Allows the Azure Function to read/write records in Dataverse. |
| Azure AD App Registration | user_impersonation | Dataverse API | Allows the custom connector to call Dataverse on behalf of the logged-in user. |
| Azure Function Host | Function Key (code) | Azure Function Endpoint | Secures the HTTP endpoint from unauthorized public access. |
Step-by-Step Application User Configuration
To configure the Dataverse Application User for the Azure Function:
- Create a custom security role in Dataverse named Azure Function Integrator.
- Grant the role Read and Write privileges on the
Accounttable, scoped to the Business Unit. - Navigate to the Power Platform Admin Center > Environments > [Your Environment] > Settings > Application users.
- Click New app user, select the User-Assigned Managed Identity, and assign the Azure Function Integrator role.
7. Pipeline Execution Internals
Understanding the internals of the Dataverse execution pipeline is critical for defining execution boundaries and preventing system failures.
+---------------------------------------------------------------------------------+
| DATAVERSE EVENT PIPELINE |
| |
| +------------------+ +------------------+ +-------------------------+ |
| | Pre-Validation | --> | Pre-Operation | --> | Main Operation | |
| | (Stage 10) | | (Stage 20) | | (Stage 30) | |
| +------------------+ +------------------+ +-------------------------+ |
+---------------------------------------------------------------------------------+
|
v
+---------------------------------------------------------------------------------+
| DATAVERSE EVENT PIPELINE |
| |
| +-----------------------------+ |
| | Post-Operation | |
| | (Stage 40) | |
| +-----------------------------+ |
+---------------------------------------------------------------------------------+
Pipeline Stages
- Pre-Validation (Stage 10): Executes before database transactions start. Used for validation logic. Webhooks registered here cannot participate in the database transaction.
- Pre-Operation (Stage 20): Executes within the database transaction before the main operation. Webhooks registered here can modify the input parameters before they are committed to the database.
- MainOperation (Stage 30): Internal platform execution (database insert/update/delete). No custom plug-ins or webhooks can be registered here.
- Post-Operation (Stage 40): Executes within the database transaction after the main operation. This is the most common stage for webhooks and Service Bus integrations.
Transaction Boundaries and Rollback
- Synchronous Steps: If a webhook is registered as a synchronous step in Stage 20 or 40, it runs within the Dataverse database transaction. If the webhook returns an HTTP status code outside the
2xxrange, or if it times out, Dataverse rolls back the entire transaction. Any database changes made during the transaction are undone. Warning: If the webhook has already made external side-effects (e.g., charged a credit card), those external actions cannot be rolled back automatically by Dataverse. Developers must implement compensating transactions. - Asynchronous Steps: Asynchronous steps execute outside the database transaction. The event is written to the
AsyncOperationtable and executed by the asynchronous service. A failure in an asynchronous webhook does not affect the original Dataverse transaction.
Sandbox Isolation Mode Restrictions
All custom plug-ins and custom workflow activities in Dataverse execute within a restricted Sandbox Isolation Mode. This environment enforces the following security and resource boundaries:
- No Local File System Access: You cannot read or write to the local disk.
- No Registry Access: You cannot access the Windows registry.
- Network Restrictions: You can only make outbound network calls via HTTP (port 80) and HTTPS (port 443). IP addresses must be public; calls to private IP addresses or localhost are blocked.
- Execution Timeout: A hard 2-minute (120 seconds) limit is enforced. If a plug-in or synchronous webhook step exceeds this limit, the platform terminates the execution thread and throws a timeout exception.
Depth and Loop-Guard Limits
To prevent infinite loops (e.g., a plug-in updates an account, which triggers the same plug-in to run again), Dataverse enforces a depth limit:
- Depth Limit: If the execution depth of a chain of plug-ins/workflows exceeds 8 within a 10-minute window, the platform terminates execution and throws an error.
- Correlation ID: The
CorrelationIdproperty in theRemoteExecutionContextis preserved across the entire execution chain, allowing developers to track and debug multi-step integrations.
8. Error Handling & Retry Patterns
Robust error handling is essential when integrating distributed systems. Transient network failures, service outages, and rate limits must be handled gracefully.
Failure Modes and Error Codes
| Error Code / Exception | Root Cause | Diagnostic Steps | Remediation |
|---|---|---|---|
429 Too Many Requests | Dataverse Service Protection Limits exceeded. | Check the Retry-After header in the response. | Implement exponential backoff using Polly in the Azure Function. |
403 Forbidden | Application User lacks privileges or Managed Identity is misconfigured. | Verify the Application User's security role in Dataverse. | Assign the correct security role with Read/Write privileges. |
503 Service Unavailable | Azure Function is offline or scaling up. | Check Azure Function App metrics and logs in Application Insights. | Configure the Function App with Always Ready instances. |
Webhook Timeout | Synchronous webhook exceeded the 2-minute limit. | Check the execution time of the Azure Function. | Offload long-running work to an asynchronous queue. |
Retry Logic with Exponential Backoff (Polly)
In your Azure Function, use the Polly library to handle transient faults when calling the Dataverse API.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Polly;
using Polly.Extensions.Http;
namespace Enterprise.Integration.Client
{
public class ResilientDataverseClient
{
private readonly HttpClient _httpClient;
public ResilientDataverseClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
/// <summary>
/// Configures a resilient retry policy with exponential backoff and jitter.
/// </summary>
public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError() // Handles 5xx and 408 errors
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests) // Handles 429
.WaitAndRetryAsync(
retryCount: 5,
sleepDurationProvider: (retryAttempt, response, context) =>
{
// Check if Dataverse returned a Retry-After header
var retryAfter = response?.Result?.Headers?.RetryAfter?.Delta;
if (retryAfter.HasValue)
{
return retryAfter.Value;
}
// Fallback to exponential backoff with jitter
var random = new Random();
double delay = Math.Pow(2, retryAttempt) + random.NextDouble();
return TimeSpan.FromSeconds(delay);
},
onRetryAsync: async (outcome, timespan, retryAttempt, context) =>
{
// Log retry attempt
Console.WriteLine($"Retry {retryAttempt} initiated after {timespan.TotalSeconds} seconds due to: {outcome.Exception?.Message ?? outcome.Result.StatusCode.ToString()}");
await Task.CompletedTask;
}
);
}
}
}
Logic Apps Error Handling: Run After and Scopes
In Azure Logic Apps, handle errors visually using Scopes and the Configure Run After setting.
+-------------------------------------------------------+
| Try Scope |
| |
| +------------------+ +----------------------+ |
| | Call Dataverse | ----> | Call Azure Function | |
| +------------------+ +----------------------+ |
+-------------------------------------------------------+
|
| (If Try Scope Fails)
v
+-------------------------------------------------------+
| Catch Scope |
| |
| +-------------------------------------------------+ |
| | Log Error to Application Insights & Send Alert | |
| +-------------------------------------------------+ |
+-------------------------------------------------------+
- Group your integration actions inside a Scope action named
Try_Scope. - Add a second Scope action immediately below it named
Catch_Scope. - Click the ellipsis (
...) on theCatch_Scopeand select Configure run after. - Check has failed, is skipped, and has timed out, then uncheck is successful. Click Done.
- Inside the
Catch_Scope, use theresult('Try_Scope')expression to parse the error details and send an alert or log it to Application Insights.
9. Performance Optimisation & Limits
To build high-scale integrations, developers must design within the platform limits of both Dataverse and Azure.
Dataverse Service Protection Limits
Dataverse enforces service protection limits per web server to ensure consistent performance for all users. These limits are evaluated within a 5-minute sliding window:
- Number of Requests: 6,000 requests per user.
- Execution Time: 20 minutes (1,200 seconds) of combined execution time per user.
- Concurrent Requests: 52 concurrent requests per user.
If any of these limits are exceeded, Dataverse returns an HTTP 429 Too Many Requests error with a Retry-After header.
Payload Size Limits
- Webhooks: The maximum payload size for a Dataverse webhook is 256 KB. If the serialized
RemoteExecutionContextexceeds this limit, Dataverse strips large properties (such asParentContext,InputParameters, and entity images) and adds the headerx-ms-dynamics-msg-size-exceededto the request. - Service Bus: The maximum payload size is 192 KB. If exceeded, properties are stripped, and the
MessageMaxSizeExceededproperty is added to the brokered message. If the payload still exceeds 192 KB after stripping, the message is not sent, and an error is logged in the System Jobs.
Optimisation Strategies
1. Batching with ExecuteMultipleRequest
When writing data back to Dataverse from an Azure Function, avoid sending individual requests. Instead, group operations into batches using ExecuteMultipleRequest or CreateMultipleRequest / UpdateMultipleRequest (available in newer SDK versions).
var requestWithResults = new ExecuteMultipleRequest()
{
Settings = new ExecuteMultipleSettings()
{
ContinueOnError = true,
ReturnResponses = true
},
Requests = new OrganizationRequestCollection()
};
foreach (var entity in entitiesToCreate)
{
CreateRequest createRequest = new CreateRequest { Target = entity };
requestWithResults.Requests.Add(createRequest);
}
ExecuteMultipleResponse response = (ExecuteMultipleResponse)serviceClient.Execute(requestWithResults);
2. Paging Large Datasets
When retrieving records from Dataverse, always use paging to avoid memory exhaustion and timeouts.
QueryExpression query = new QueryExpression("account")
{
ColumnSet = new ColumnSet("name", "revenue"),
PageInfo = new PagingInfo
{
Count = 500,
PageNumber = 1,
PagingCookie = null
}
};
while (true)
{
EntityCollection results = serviceClient.RetrieveMultiple(query);
foreach (var entity in results.Entities)
{
// Process record
}
if (results.MoreRecords)
{
query.PageInfo.PageNumber++;
query.PageInfo.PagingCookie = results.PagingCookie;
}
else
{
break;
}
}
10. ALM & Deployment Checklist
Moving serverless integrations across environments (Dev -> Test -> Prod) must be fully automated using CI/CD pipelines.
+-----------------+ +------------------+ +-----------------+
| DEVELOPMENT | | AZURE DEVOPS | | PRODUCTION |
| | | | | |
| Export Solution | ---> | Run Solution | ---> | Import Solution |
| (Unmanaged) | | Checker & Pack | | (Managed) |
+-----------------+ +------------------+ +-----------------+
|
v
+------------------+
| Inject Env Vars |
| & Conn References|
+------------------+
Step-by-Step ALM Checklist
- Export Solution: Export your solution from the development environment as a Managed solution.
- Generate Settings File: Use the PAC CLI to generate a deployment settings file:
pac solution create-settings --solution-zip MySolution.zip --settings-file settings.json
- Configure Target Values: Edit the
settings.jsonfile to include the target environment's connection IDs and environment variable values (e.g., the production Azure Function URL). - Commit to Source Control: Commit the solution zip and the environment-specific settings files to your Git repository.
- Execute Pipeline: Run your Azure DevOps or GitHub Actions pipeline to import the solution into the target environment, passing the settings file as a parameter.
Azure DevOps Pipeline YAML Snippet
This YAML snippet demonstrates how to import a solution using the Power Platform Build Tools and inject environment-specific settings.
trigger:
- main
pool:
vmImage: 'windows-latest'
variables:
solutionName: 'ContosoIntegration'
steps:
# 1. Install Power Platform Tool Installer
- task: microsoft-IsvExpTools.PowerPlatform-BuildTools.tool-installer.PowerPlatformToolInstaller@2
displayName: 'Power Platform Tool Installer'
# 2. Import Solution to Target Environment
- task: microsoft-IsvExpTools.PowerPlatform-BuildTools.import-solution.PowerPlatformImportSolution@2
displayName: 'Import Solution to Production'
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: 'ProductionServiceConnection'
SolutionInputFile: '$(Build.SourcesDirectory)/solutions/$(solutionName)_managed.zip'
UseDeploymentSettingsFile: true
DeploymentSettingsFile: '$(Build.SourcesDirectory)/config/prod-settings.json'
AsyncOperation: true
MaxAsyncWaitTime: '60'
11. Common Pitfalls & Troubleshooting Guide
1. Webhook Timeout (Synchronous Step)
- Symptom: Users experience slow performance, followed by an error: "The plug-in execution has failed because the Webhook call timed out."
- Root Cause: The webhook was registered as a synchronous step, and the Azure Function took longer than the 2-minute execution limit (or the 10-second internal webhook timeout limit) to respond.
- Diagnostic Steps: Check the execution logs of the Azure Function in Application Insights. Verify if the step is registered as synchronous in the Plugin Registration Tool.
- Fix: Re-register the step as Asynchronous. If synchronous execution is required, optimize the Azure Function to return an immediate
202 Acceptedresponse and process the work in the background.
2. Payload Truncation (256 KB Limit)
- Symptom: The Azure Function throws a
KeyNotFoundExceptionwhen trying to readInputParametersor entity images from theRemoteExecutionContext. - Root Cause: The payload size exceeded 256 KB. Dataverse stripped the input parameters and images to reduce size, adding the
x-ms-dynamics-msg-size-exceededheader. - Diagnostic Steps: Check the HTTP request headers received by the Azure Function. Look for
x-ms-dynamics-msg-size-exceeded. - Fix: Modify the integration to pass only the record ID. Have the Azure Function query Dataverse back using the
ServiceClientto retrieve the full record details.
3. Managed Identity Authentication Failure
- Symptom: The Azure Function logs show: "Caller is not authorized to perform action... Status: 403 Forbidden" when calling Dataverse.
- Root Cause: The User-Assigned Managed Identity was not registered as an Application User in Dataverse, or was not assigned a security role.
- Diagnostic Steps: Verify the Client ID of the Managed Identity matches the Application ID of the Application User in Dataverse. Check the assigned security roles.
- Fix: Create the Application User in the target Dataverse environment and assign a security role with sufficient privileges.
4. Infinite Loop / Depth Limit Exceeded
- Symptom: Dataverse operations fail with the error: "This workflow or plug-in has been terminated because it exceeded the maximum depth limit."
- Root Cause: The Azure Function updates the same record that triggered it, causing an infinite loop.
- Diagnostic Steps: Check the
Depthproperty in theRemoteExecutionContextreceived by the Azure Function. If it increases with each call, a loop is occurring. - Fix: In the Azure Function, check the
InitiatingUserIdor add a custom flag to the update request to prevent re-triggering. Alternatively, configure the webhook step's Filtering Attributes to exclude the attribute updated by the Azure Function.
5. Environment Variable Caching
- Symptom: After importing a solution with updated environment variable values, the custom connector or flow continues to call the old Azure Function URL.
- Root Cause: Power Platform caches environment variable values for up to 15 minutes.
- Diagnostic Steps: Check the value of the environment variable in the target environment. If correct, but the old URL is still called, caching is the cause.
- Fix: Turn the consuming flow off and on again, or perform a "Publish All Customizations" in the target environment to clear the cache.
6. Missing Key Vault Permissions
- Symptom: Solution import fails with: "User is not authorized to read secrets from Key Vault..."
- Root Cause: The user importing the solution, or the Dataverse Service Principal, lacks read permissions on the Azure Key Vault.
- Diagnostic Steps: Check the Key Vault Access Policies or Azure RBAC configuration.
- Fix: Grant the Key Vault Secrets User role to both the importing user and the Dataverse Service Principal (
00000007-0000-0000-c000-000000000000).
7. Logic App Concurrency Throttling
- Symptom: Logic App runs are delayed, and some trigger executions fail with a
429error. - Root Cause: The Logic App trigger is receiving a high volume of events and hitting concurrency limits.
- Diagnostic Steps: Check the Logic App run history and trigger history. Look for throttled runs.
- Fix: Enable Split On (debatching) on the trigger settings to process array items in parallel, or configure the trigger's concurrency limit to a specific value.
8. Service Bus Message Size Exceeded
- Symptom: Asynchronous system jobs in Dataverse show a status of Failed with an error: "Message size exceeded."
- Root Cause: The serialized payload posted to Azure Service Bus exceeded the 192 KB limit.
- Diagnostic Steps: Check the System Jobs in Dataverse (
Settings>System Jobs) and filter by failed integration jobs. - Fix: Reduce the number of entity images registered on the step. Only include the attributes required for processing.
9. Cold Start Latency
- Symptom: The first webhook call of the day fails with a timeout, but subsequent calls succeed.
- Root Cause: The Azure Function is hosted on a Consumption plan and scaled to zero, causing a cold start delay that exceeds the webhook timeout.
- Diagnostic Steps: Check the execution duration of the failed run in Application Insights.
- Fix: Migrate the Function App to a Premium or Flex Consumption plan and configure Always Ready instances.
10. Missing Filtering Attributes
- Symptom: The Azure Function is triggered constantly, even when unrelated fields on the record are updated, causing high Azure costs.
- Root Cause: The webhook step was registered without filtering attributes.
- Diagnostic Steps: Open the step registration in the Plugin Registration Tool and check the Filtering Attributes field.
- Fix: Edit the step and select only the specific attributes that should trigger the integration.
12. Exam Focus: Key Facts & Edge Cases
To prepare for the PL-400 exam, memorise these critical facts, limits, and behaviors:
- Webhook Timeout: Synchronous webhooks have a hard 10-second timeout limit within the Dataverse pipeline. If exceeded, the transaction rolls back.
- Payload Size Limits:
- Webhooks: 256 KB (strips properties and adds
x-ms-dynamics-msg-size-exceededheader if exceeded). - Service Bus: 192 KB (strips properties and adds
MessageMaxSizeExceededproperty if exceeded).
- Webhooks: 256 KB (strips properties and adds
- Service Protection Limits: Evaluated in a 5-minute sliding window (6,000 requests, 20 minutes execution time, 52 concurrent requests per web server).
- Depth Limit: Dataverse blocks execution if the depth exceeds 8 within a 10-minute window.
- RemoteExecutionContext Serialization: Webhooks always transmit payloads in JSON format. Service Bus and Event Hubs support JSON, XML, and Binary.
- Managed Identity CLI: Use
pac managed-identity createto link a User-Assigned Managed Identity to a Dataverse component. - Key Vault Secret Environment Variables: Require the
Microsoft.PowerPlatformresource provider to be registered in the Azure subscription, and the Dataverse Service Principal to have the Key Vault Secrets User role. - Custom Connector Syntax: To reference an environment variable in a custom connector, use the syntax:
@environmentVariables("environmentVariableName"). - Plugin Registration Tool (PRT): Webhooks must be registered using the PRT. You must provide the Endpoint URL and Authentication (HttpHeader, WebhookKey, or HttpQueryString).
- WebhookKey Authentication: This option is specifically designed for Azure Functions because it automatically maps the key value to the
codequery string parameter expected by the Azure Function host.