Process an equipment request with Azure Functions
The equipment provisioning system has to check a slow external availability service before it can start fulfilling an approved request. A synchronous Dataverse plug-in would have to block the caller until that external service finally replies. In this exercise, you move that work out of Dataverse entirely and into an Azure Function: an HTTP-triggered function that an event-driven caller can invoke, which checks availability, updates the request, and returns, all without holding open a transaction inside Dataverse. The function authenticates to Dataverse using its own system-assigned managed identity, not a client secret or a signed-in user.
You also deploy, but don't write, a second function that runs on a schedule instead of in response to an event: a nightly job that recalculates how many devices each employee has used against their allowance. Running it once lets you see the difference between the two trigger types this exercise covers: one that reacts to a request, and one that runs on a timer.
In this exercise, you will:
- Deploy a storage account, a Flex Consumption hosting plan, and a function app with a system-assigned managed identity, from a supplied Bicep template
- Create a Dataverse application user for that managed identity and assign it the Equipment Integration security role
- Complete an HTTP-triggered function that connects to Dataverse with the managed identity and updates a request's status and availability
- Deploy the function app and trigger it against a real request to watch its status change
- Optionally, run a supplied timer-triggered function once to correct a stale allowance
This exercise should take approximately 30 minutes to complete.
Prerequisites
To complete this exercise, you need:
- An Azure subscription{:target="_blank"} at
https://azure.microsoft.com/free/, with permission to create a resource group and deploy function app resources into it - A Power Platform developer environment with the Equipment Provisioning (base) solution installed (confirmed in the first task below)
- Visual Studio Code{:target="_blank"} at
https://code.visualstudio.com/ - The .NET SDK{:target="_blank"} at
https://dotnet.microsoft.com/download, version 8 or later - Azure Functions Core Tools{:target="_blank"} at
https://learn.microsoft.com/azure/azure-functions/functions-run-local, version 4 - The Azure CLI{:target="_blank"} at
https://learn.microsoft.com/cli/azure/install-azure-cli
Confirm the base solution is installed and reset the lab data
Every exercise that uses Dataverse starts the same way: confirm the shared base solution is present, then reset the seed data so the records this exercise uses are in their starting state.
- Sign in to the Power Apps maker portal{:target="_blank"} at
https://make.powerapps.com, and select your developer environment. - Select Solutions, and confirm that Equipment Provisioning (base) is listed. If it isn't, follow the import steps in the setup instructions, then come back to this exercise.
- Open the Equipment Provisioning app.
- In the site map, under Equipment provisioning, select Requests.
- On the command bar, select Reset lab data. Wait for the progress indicator, then review the summary in the Lab data reset dialog.
- Select Refresh.
You should see a dialog summarizing how many employees, devices, requests, and allowances were reset. You'll use request REQ-SEED-004 and allowance EMP-002-CURRENT later in this exercise, so this step guarantees both start from a known state.
Review the starter function project and deployment template
Before you write any code, look at what's already there. This exercise uses the lab files you cloned during setup, in the Allfiles/03-azure-functions-request folder. If you didn't clone the repository, clone it from GitHub{:target="_blank"} at https://github.com/MicrosoftLearning/AB-400-microsoft-power-platform-developer.git.
-
Open the
Allfiles/03-azure-functions-requestfolder in Visual Studio Code. -
Open
EquipmentRequestFunctions/CheckRequestAvailability.cs. This is the HTTP-triggered function you'll complete. It takes a request number, and has four comment placeholders marking where you'll add code later in this exercise. -
Open
EquipmentRequestFunctions/SlowAvailabilityService.cs. This small helper simulates the organization's real external availability service with a delay, which is the reason this work doesn't belong in a synchronous plug-in. -
Open
EquipmentRequestFunctions/RecalculateAllowances.cs. This timer-triggered function is already complete. It recalculates every allowance's used and remaining counts from the requests that are actually Fulfilled. You'll deploy and run it once, but you won't change its code. -
Open
deploy/main.bicep. Review the resources it provisions: a storage account, a Flex Consumption hosting plan, and a function app with a system-assigned managed identity already enabled. Every resource name is generated from the resource group, so the template produces the same names every time you deploy it into the same resource group.If you only need this file, download main.bicep{:target="_blank"} from
https://raw.githubusercontent.com/MicrosoftLearning/AB-400-microsoft-power-platform-developer/main/Allfiles/03-azure-functions-request/deploy/main.bicep.
You should now be able to tell the two functions apart: CheckRequestAvailability is the one you'll complete, and RecalculateAllowances is the one you'll only deploy and run.
Deploy the Azure resources for the function app
Now provision the storage account, hosting plan, and function app that your code will run in.
-
In a terminal, sign in to Azure:
az login -
Create a new resource group named
rg-ab400-lab03, or select an existing one:az group create --name rg-ab400-lab03 --location <region>Replace
<region>with a region close to you, such aseastus2. -
Change to the
deployfolder, and deploy the template:cd Allfiles/03-azure-functions-request/deploy az deployment group create --resource-group rg-ab400-lab03 --template-file main.bicep --query properties.outputs -
From the command's output, note the
functionAppNamevalue (for example,func-eqp-abc1234567). You'll use it in every remaining task.
You should see a JSON object with functionAppName, functionAppDefaultHostName, functionAppPrincipalId, and storageAccountName values. Function app host names must be unique across every Azure subscription in the world, so on the rare chance this name is already taken, create a new resource group with a different name and run the deployment again.
Create a Dataverse application user for the managed identity
An Azure resource's managed identity can't sign in to Dataverse until it exists there as an application user with a security role. Power Platform allows only one application user per Microsoft Entra application per environment, so this step is safe to repeat.
-
Find the managed identity's application ID, using the
functionAppPrincipalIdvalue from the previous task:az ad sp show --id <functionAppPrincipalId> --query appId --output tsv -
Sign in to the Power Platform admin center{:target="_blank"} at
https://admin.powerplatform.microsoft.com. -
Select Environments, then select your developer environment.
-
Select Settings > Users + permissions > Application users.
-
Select + New app user, or select the existing application user for this managed identity if one is already listed.
-
Select + Add an app. Search for the application ID you noted in step 1, select it, and select Add.
Note: For a managed identity, search by its application ID, not its name. Managed identities don't appear in the application list by name.
-
Under Business unit, select a business unit.
-
Enter an email address, for example
azure-functions-lab03@example.com. -
Select the edit icon next to security roles, and add Equipment Integration and Basic User. Select Save, then confirm the role assignment.
-
Select Create.
You should see the new application user listed with the Equipment Integration role. If you selected an existing application user instead, confirm it already has that role.
Complete the Dataverse connection and status update logic
Now wire the function app's managed identity into a Dataverse connection, and use that connection to update a request.
-
In
DataverseConnection.cs, find the comment Acquire tokens from the function app's managed identity, and add the following code:var credential = new DefaultAzureCredential(); return new ServiceClient(instanceUri, async (string resource) => { // ServiceClient passes the full Organization service URL here (the XRMServices path // and an SDKClientVersion query string), not a bare origin, so build the scope from // just the scheme and host. var audience = new Uri(resource).GetLeftPart(UriPartial.Authority); var tokenRequestContext = new TokenRequestContext(new[] { $"{audience}/.default" }); var token = await credential.GetTokenAsync(tokenRequestContext); return token.Token; });This passes a token provider function to the Dataverse
ServiceClient, instead of a connection string. Every time the client needs a token, it asks the function app's managed identity for one. TheresourcevalueServiceClientpasses to this callback is the full Organization service URL, complete with path and query string — appending/.defaultto it directly produces an invalid OAuth scope, so the callback first reduces it to just the scheme and host. -
In
CheckRequestAvailability.cs, find the comment Connect to Dataverse by using the function app's managed identity, and add the following code:using var serviceClient = DataverseConnection.CreateServiceClient(environmentUrl);This creates the connection using the method you just completed, authenticating with the function app's managed identity.
-
Find the comment Find the request by its request number, and add the following code:
var keys = new KeyAttributeCollection { { "eqp_name", requestNumber } }; var retrieveRequest = new RetrieveRequest { Target = new EntityReference("eqp_request", keys), ColumnSet = new ColumnSet("eqp_requeststatus", "eqp_availability") }; var request = ((RetrieveResponse)serviceClient.Execute(retrieveRequest)).Entity;This looks up the request by its request number (
REQ-SEED-004, for example) instead of its internal ID, using the alternate key Dataverse already defines on that column. -
Find the comment Simulate the slow external availability service, and add the following code:
var isAvailable = await SlowAvailabilityService.CheckAsync(requestNumber);This calls the supplied helper, which pauses for a few seconds before reporting whether the device is available, standing in for the real external service.
-
Find the comment Update the request's status and availability columns, and add the following code:
var update = new Entity("eqp_request", request.Id); if (isAvailable) { update["eqp_requeststatus"] = new OptionSetValue(100000003); // In fulfillment update["eqp_availability"] = new OptionSetValue(100000001); // Available update["eqp_processingnote"] = $"Availability confirmed by the Azure Function on {DateTime.UtcNow:u}."; } else { update["eqp_availability"] = new OptionSetValue(100000002); // Unavailable update["eqp_processingnote"] = $"The requested device type isn't currently available (checked {DateTime.UtcNow:u})."; } update["eqp_availabilitycheckedon"] = DateTime.UtcNow; serviceClient.Update(update);If the device is available, this moves the request to In fulfillment; otherwise, it records Unavailable and leaves the request Approved for a coordinator to follow up. Either way, it stamps when the check happened and why.
As you add code, be sure to maintain the correct indentation. Use the existing comments as a guide, entering the new code at the same level of indentation.
Tip: If you're not sure what to write for one of these steps, ask GitHub Copilot to explain the surrounding code or suggest the next line. The comment above each placeholder is written to be a useful prompt on its own.
-
Save all your changed files, and build the project to confirm it compiles:
dotnet build
You should see Build succeeded with no errors.
Deploy and configure the function app
With the code complete, deploy it to the function app you provisioned earlier, then tell it which Dataverse environment to connect to.
-
From the
EquipmentRequestFunctionsfolder, publish the project:func azure functionapp publish <functionAppName> --dotnet-isolatedReplace
<functionAppName>with the value you noted earlier. The--dotnet-isolatedflag identifies the project's runtime without needing a local settings file. Publishing can take a minute or two. When it finishes, the output lists both functions and, forCheckRequestAvailability, an invoke URL similar tohttps://<functionAppName>.azurewebsites.net/api/checkrequestavailability. -
Set the function app's Dataverse connection setting:
az functionapp config appsettings set --name <functionAppName> --resource-group rg-ab400-lab03 --settings DATAVERSE_ENVIRONMENT_URL=<environment-url>Replace
<environment-url>with your Dataverse environment's URL, for examplehttps://yourorg.crm.dynamics.com. You can find it on the environment's details page in the Power Platform admin center. -
Verify the stored Dataverse URL:
az functionapp config appsettings list --name <functionAppName> --resource-group rg-ab400-lab03 --query "[?name=='DATAVERSE_ENVIRONMENT_URL'].value" --output tsvThe setting command might return
nullinstead of echoing the value. This read-only query should print the environment URL you supplied.
Trigger the function and verify the result
This is the moment the whole exercise has been building toward: invoke the deployed function against a real request, and watch Dataverse change in response.
-
Open the following URL in a browser, or call it with a tool such as curl, replacing
<functionAppName>with your function app's name:https://<functionAppName>.azurewebsites.net/api/checkrequestavailability?requestNumber=REQ-SEED-004 -
Wait a few seconds. The function deliberately pauses to simulate the slow external service before it responds.
-
Confirm you see a response similar to
Finished checking availability for REQ-SEED-004. -
Back in the Equipment Provisioning app, open Requests, and open
REQ-SEED-004.
You should see Request status has changed from Approved to In fulfillment, Availability has changed from Not checked to Available, and Availability checked on and Processing note are both populated. That delay you waited through is exactly why this work runs in an Azure Function instead of inside a Dataverse plug-in: a plug-in would have held the save operation open for those same few seconds.
Try this: The supplied
RecalculateAllowancesfunction is already deployed. In the Azure portal{:target="_blank"} athttps://portal.azure.com, open your function app, select Functions, select RecalculateAllowances, select Code + Test, and use the Test/Run pane to run it once on demand instead of waiting for its schedule. Afterward, open the AllowanceEMP-002-CURRENTin the Equipment Provisioning app: its Devices used and Remaining values correct from the stale 2 and 0 to 0 and 2, and Last recalculated on updates. This isn't required to complete the exercise, but it's worth about two minutes if you have time, because it's the only place in this exercise you see a scheduled trigger run.
Summary
In this exercise, you deployed an Azure Functions app to a Flex Consumption plan using a supplied Bicep template, and connected it to Dataverse through a system-assigned managed identity instead of a client secret. You completed an HTTP-triggered function that looked up a request by its alternate key, simulated a slow external dependency, and updated the request's status and availability columns. You also deployed a supplied timer-triggered function and, optionally, ran it on demand to see a scheduled trigger correct a stale allowance.
You have successfully completed this exercise.
Clean up
If you've finished exploring the Azure resources used in this exercise, delete them to avoid incurring unnecessary costs.
- Open the Azure portal{:target="_blank"} at
https://portal.azure.com, and view the contents of the resource group you used in this exercise (rg-ab400-lab03, unless you chose a different name). - On the toolbar, select Delete resource group.
- Enter the resource group name, and confirm that you want to delete it.
Deleting the resource group also deletes the function app's managed identity. You can leave the Dataverse application user in place; it's unlicensed and harmless to keep. If you later redeploy this exercise's resources into the same or a different resource group, the new function app gets a new managed identity, so the existing application user will no longer authenticate successfully. In that case, remove the old application user and create a new one by repeating the earlier task.