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 in the same Microsoft Entra tenant as the Azure subscription, 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. Run this command only if you're creating a new group:az group create --name rg-ab400-lab03 --location <region>Replace
<region>with a region close to you, such aseastus2. Record the selected group's name. If you're using an existing group, record its resources and deployments before proceeding so you can preserve them during cleanup. -
From the repository root, change to the
deployfolder and preview the deployment:cd Allfiles/03-azure-functions-request/deploy az deployment group what-if --resource-group <resource-group> --template-file main.bicepReplace
<resource-group>here and in the remaining commands with the group you selected. Review the preview before deploying. Don't proceed if it changes or removes unrelated resources. If you're repeating the exercise, verify that any existing resources being updated belong to this exercise. If Bicep is missing, runaz bicep install, then repeat the preview. -
Deploy the reviewed template:
az deployment group create --resource-group <resource-group> --template-file main.bicep --query properties.outputsThis creates the resources shown in the preview. Record the deployment name and the function app, hosting plan, and storage account names shown in the deployment details for cleanup.
-
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.
-
Check for an existing application user whose App ID matches the managed identity's application ID. If one exists, confirm it's active and has Equipment Integration and Basic User, then skip the remaining creation steps. Otherwise, select + New app user.
-
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.
-
If the form has an email address field, enter
azure-functions-lab03@example.com. Some versions of the admin center omit this field for application users. -
Select the edit icon next to security roles, and add Equipment Integration and Basic User. Select Save, then verify the confirmation lists those two roles before confirming.
-
Select Create.
You should see the application user listed with Equipment Integration and Basic User.
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 <resource-group> --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 <resource-group> --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.
Note: The supplied template doesn't provision Application Insights, so the portal's invocation log view may be unavailable. Verify the deployed function's HTTP response and the saved Dataverse row; a successful build or deployment alone doesn't prove the function ran.
Try this: The supplied
RecalculateAllowancesfunction is already deployed. To run it on demand, open the Azure portal{:target="_blank"} athttps://portal.azure.com. For this optional browser-based test, configure cross-origin resource sharing (CORS) if the portal's origin isn't already allowed:
- In the Azure portal, open your function app.
- Open CORS. Under Allowed Origins, add
https://portal.azure.comif it isn't already present. Preserve existing entries and don't use the wildcard*. If the portal test reports a different blocked Microsoft portal origin, allow that exact origin rather than all origins.- Select Save.
Then 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 Allowance
EMP-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 review the resources you created for this exercise against the inventory you recorded before deployment. - If you created the resource group solely for this exercise and it contains no unrelated resources, select Delete resource group, enter its name, and confirm deletion. Don't delete an existing or shared resource group.
- If you used an existing group, delete only the function app, Flex Consumption hosting plan, and storage account created by this exercise, using the names you recorded from the deployment. Don't delete resources that existed before this exercise. Under Deployments, remove only the deployment record you created. A deployment record's deletion doesn't delete its resources.
- If you retained the resource group, refresh its resource and deployment lists. Verify that only the exercise's additions are gone and any original resources and deployment records remain.
Deleting the function app also deletes its managed identity. You can leave the Dataverse application user in place, but the deleted identity can no longer use it. If you later redeploy, the new function app gets a new managed identity; create its application user by repeating the earlier task. Don't change unrelated application users or shared solutions.