Search code examples
c#azure-functions.net-8.0dataverseazure-functions-isolated

Can't get Dataverse ServiceClient to work properly in Azure Function


I have a set of HTTP-triggered functions in an Azure Functions app (.NET 8, isolated worker model) that integrate with Dynamics 365 using the Dataverse ServiceClient (v1.1.17). The issue I'm encountering is when I send multiple requests, the underlying HttpClient gets disposed of unexpectedly.

At first, I thought this issue was related to the lifecycle of the ServiceClient. However, I've tested it by registering it as both scoped and singleton, but there was no difference. I've tried creating a new instance and disposing of it after each use. I even tried a "trick" I found in the GitHub repo CdsWeb that wraps the client and creates a reusable instance copy.

I've run the same code in a fresh minimal API, and it works just fine. The same goes for running it inside a console app inside a loop. When load testing it with k6 (100 VUs for 20s) it results in 1 completed and 99 interrupted iterations. However, when implemented in a minimal API, it results in 100 completed and 0 interrupted iterations.

Running the function in Azure (consumption-based) produces the same error. It also regularly produces a TaskCanceledException.

This is a bare minimum test:

var query = new QueryExpression(entityName: "contact")
{
    TopCount = 1,
    ColumnSet = new ColumnSet("contactid", "firstname")
};

// serviceClient injected as a singleton. Access token is cached in memory and reused.
var results = await serviceClient.RetrieveMultipleAsync(query);
var contactEntity = results.Entities.SingleOrDefault();

This is how the ServiceClient is setup:

// Adding it to servicecollection
services.AddSingleton<IOrganizationServiceAsync, ServiceClient>(provider =>
{
    var appSettings = provider.GetRequiredService<IOptions<AppSettings>>();
    var cache = provider.GetRequiredService<IMemoryCache>();

    var managedIdentity = new DefaultAzureCredential(new DefaultAzureCredentialOptions
    {
        // Using Managed Identity running in Azure
        ManagedIdentityClientId = appSettings.Value.ManagedIdentityClientId,
        ExcludeManagedIdentityCredential = false,

        // Using Visual Studio/VS Code credentials for local devlopment
        ExcludeVisualStudioCredential = false,
        ExcludeVisualStudioCodeCredential = false
    });
    var environment = appSettings.Value.DataverseInstanceUrl;

    return new ServiceClient(
            tokenProviderFunction: f => GetToken(environment, managedIdentity, cache),
            instanceUrl: new Uri(environment),
            useUniqueInstance: true);
});

// Getting access token and caching it for 50 minutes
async static Task<string> GetToken(string environment, DefaultAzureCredential credential, IMemoryCache cache)
{
    var accessToken = await cache.GetOrCreateAsync(environment, async (cacheEntry) =>
    {
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(50);
        var token = await credential.GetTokenAsync(new TokenRequestContext([$"{environment}/.default"]));
        return token;
    });
    return accessToken.Token;
}

Solution

  • NuGet package Microsoft.PowerPlatform.Dataverse.Client version 1.1.17 is built on .NET 6.0. It is important to note that it has a dependency on assembly System.Text.Json Version 7.0.0.0, which is part of .NET 7. However, .NET 7 is not available on Azure Functions servers. See this discussion on GitHub and on Power Apps Community Forums.

    An easy fix would be to base your Azure Function project on .NET 6 and use NuGet package Microsoft.PowerPlatform.Dataverse.Client version 1.1.14 instead of the latest.

    Microsoft plans to add .NET 8 support at the end of this year.