You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Azure Function中Dataverse ServiceClient多请求异常问题求助

.NET 8独立工作者模型Azure Function集成Dataverse多请求异常解决

问题背景

基于.NET 8独立工作者模型开发的Azure Function应用,包含多个HTTP触发函数,通过Dataverse ServiceClient(v1.1.17)对接Dynamics 365。并发请求时底层HttpClient会被意外释放,频繁抛出TaskCanceledException。

试过这些方法都无效:

  • 将ServiceClient注册为Scoped或Singleton,结果无差异
  • 每次请求新建ServiceClient实例并手动释放
  • 参考CdsWeb仓库的封装方式,创建可复用实例副本

测试对比

  • 相同代码在Minimal API和控制台应用循环运行完全正常
  • k6负载测试(100虚拟用户运行20秒):
    • Azure Function场景仅1次迭代成功,99次中断
    • Minimal API场景100次迭代全部完成
  • 部署到Azure消费计划后问题依然存在

核心代码

测试代码

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

// 注入的单例ServiceClient,AccessToken已在内存缓存复用
var results = await serviceClient.RetrieveMultipleAsync(query);
var contactEntity = results.Entities.SingleOrDefault();

ServiceClient注册配置

// 注册到服务集合
services.AddSingleton<IOrganizationServiceAsync, ServiceClient>(provider =>
{
    var appSettings = provider.GetRequiredService<IOptions<AppSettings>>();
    var cache = provider.GetRequiredService<IMemoryCache>();

    var managedIdentity = new DefaultAzureCredential(new DefaultAzureCredentialOptions
    {
        // Azure环境使用托管标识
        ManagedIdentityClientId = appSettings.Value.ManagedIdentityClientId,
        ExcludeManagedIdentityCredential = false,
        // 本地开发使用VS/VS Code凭据
        ExcludeVisualStudioCredential = false,
        ExcludeVisualStudioCodeCredential = false
    });
    var environment = appSettings.Value.DataverseInstanceUrl;

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

// 获取AccessToken并缓存50分钟
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;
}

解决办法

1. 关闭useUniqueInstance参数

将ServiceClient构造中的useUniqueInstance改为false,该参数强制使用唯一HttpClient实例,在Azure Function动态资源环境下易被回收或出现线程冲突:

return new ServiceClient(
        tokenProviderFunction: f => GetToken(environment, managedIdentity, cache),
        instanceUrl: new Uri(environment),
        useUniqueInstance: false); // 修改此处

2. 改用Scoped生命周期

放弃单例注册,将ServiceClient注册为Scoped,让每个请求拥有独立实例,避免多线程共享冲突:

services.AddScoped<IOrganizationServiceAsync, ServiceClient>(provider =>
{
    // 保留原初始化逻辑,useUniqueInstance设为false
    var appSettings = provider.GetRequiredService<IOptions<AppSettings>>();
    var cache = provider.GetRequiredService<IMemoryCache>();

    var managedIdentity = new DefaultAzureCredential(new DefaultAzureCredentialOptions
    {
        ManagedIdentityClientId = appSettings.Value.ManagedIdentityClientId,
        ExcludeManagedIdentityCredential = false,
        ExcludeVisualStudioCredential = false,
        ExcludeVisualStudioCodeCredential = false
    });
    var environment = appSettings.Value.DataverseInstanceUrl;

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

3. 手动控制HttpClient生命周期

自行注册HttpClient单例,替换ServiceClient内部的HttpClient,避免依赖其内部管理逻辑:

services.AddSingleton<HttpClient>();
services.AddSingleton<IOrganizationServiceAsync, ServiceClient>(provider =>
{
    var appSettings = provider.GetRequiredService<IOptions<AppSettings>>();
    var cache = provider.GetRequiredService<IMemoryCache>();
    var httpClient = provider.GetRequiredService<HttpClient>();

    var managedIdentity = new DefaultAzureCredential(new DefaultAzureCredentialOptions
    {
        ManagedIdentityClientId = appSettings.Value.ManagedIdentityClientId,
        ExcludeManagedIdentityCredential = false,
        ExcludeVisualStudioCredential = false,
        ExcludeVisualStudioCodeCredential = false
    });
    var environment = appSettings.Value.DataverseInstanceUrl;

    var serviceClient = new ServiceClient(
            tokenProviderFunction: f => GetToken(environment, managedIdentity, cache),
            instanceUrl: new Uri(environment),
            useUniqueInstance: false);
    // 替换内部HttpClient
    serviceClient.HttpClient = httpClient;
    return serviceClient;
});

4. 添加重试策略

使用Polly添加重试逻辑,处理TaskCanceledException这类临时异常:

// 注册Polly重试策略
services.AddPolicyRegistry()
        .Add("DataverseRetry", Policy.Handle<TaskCanceledException>()
                                     .Or<HttpRequestException>()
                                     .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))));

// 在函数中使用策略
var retryPolicy = _policyRegistry.Get<IAsyncPolicy>("DataverseRetry");
await retryPolicy.ExecuteAsync(async () =>
{
    var results = await serviceClient.RetrieveMultipleAsync(query);
    contactEntity = results.Entities.SingleOrDefault();
});

原因总结

Azure Function的消费计划(包括本地宿主)会动态调整资源,单例ServiceClient内部的HttpClient易被宿主资源回收机制误释放;加上useUniqueInstance: true强制使用单一实例,高并发下会出现连接池耗尽或线程安全问题,最终导致HttpClient被异常释放。

内容的提问来源于stack exchange,提问作者camlCase

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 01:52:18