C#实现依赖型顺序API调用及令牌传递方案问询
问题描述
我需要借助Azure Function App每日执行一串依赖顺序的API调用,最终将文件下载至存储账户。目前已实现AuthHttpTrigger函数,可通过授权码获取refresh token并存储到Azure存储表(也可改用密钥保管库),该功能运行正常。
接下来需要完成以下步骤:
- 读取存储的refresh token,调用API获取新的access token,并将返回的新refresh token更新至存储表;
- 使用access token调用API创建报告,生成reportId;
- 携带access token和reportId调用API获取文件的安全下载链接(需通过定时器轮询直到链接非空)。
由于每个API调用依赖前一个的输出参数,想知道有没有简便的顺序调用实现方式?我当前思路是创建另一个函数,为每个API调用拆分任务,使用await Task和Task.WhenAll,同时用定时器等待下载链接。
最终需求:手动触发一次授权流程后,利用refresh token每日自动更新(access token每日过期),完成上述完整流程。
涉及的API:
/api/AuthHttpTrigger- 手动交互传入授权码,返回access token、refresh token和id token;https://login.windows.net/{app id}/oauth2/token- 传入refresh token获取新的access token;https://api.partnercenter.microsoft.com/insights/v1/mpn/ScheduledReport- 传入access token创建报告,生成reportId;https://api.partnercenter.microsoft.com/insights/v1/mpn/ScheduledReport/execution/{report id}- 传入access token和reportId获取下载链接。
实现方案建议
- 用Timer Trigger作为每日执行入口:创建Timer Trigger函数,配置CRON表达式(如
0 0 0 * * *实现每日凌晨执行),作为整个流程的启动点。 - 顺序调用直接用
await链式执行:因每个步骤依赖前一步输出,无需Task.WhenAll(该方法适用于并行任务),直接用await逐个调用即可,逻辑清晰且实现简单。 - 轮询下载链接添加重试机制:用循环+
Task.Delay实现轮询,同时设置最大重试次数和间隔时间,避免无限循环。 - 完善存储表的token读取逻辑:现有
AzureStorageClient缺少读取refresh token的方法,需补充该方法获取存储的token。
完整代码实现
1. 补充AzureStorageClient的token读取方法
using Microsoft.Azure.Cosmos.Table; using System.Threading.Tasks; namespace PartnerCenterAPI.Properties { public class AzureStorageClient { private readonly CloudTable refreshTokenTable; public AzureStorageClient(string connectionString) { CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString); CloudTableClient tableClient = storageAccount.CreateCloudTableClient(new TableClientConfiguration()); refreshTokenTable = tableClient.GetTableReference("refreshTokens"); refreshTokenTable.CreateIfNotExists(); } public async Task<RefreshTokenEntity> AddOrUpdateRefreshToken(string audience, string userId, string refreshToken) { RefreshTokenEntity tokenEntity = new(audience, userId) { RefreshToken = refreshToken }; TableResult tableResult = await refreshTokenTable.ExecuteAsync(TableOperation.InsertOrReplace(tokenEntity)); return tableResult.Result as RefreshTokenEntity; } // 新增:读取存储的refresh token public async Task<RefreshTokenEntity> GetRefreshToken(string audience, string userId) { TableOperation retrieveOperation = TableOperation.Retrieve<RefreshTokenEntity>(audience, userId); TableResult result = await refreshTokenTable.ExecuteAsync(retrieveOperation); return result.Result as RefreshTokenEntity; } } }
2. 创建PartnerCenterApiClient封装报告相关API
using Newtonsoft.Json; using System; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; namespace PartnerCenterAPI.Properties { public class PartnerCenterApiClient { private static readonly HttpClient httpClient = new(); private readonly string baseUrl = "https://api.partnercenter.microsoft.com/insights/v1/mpn"; public async Task<string> CreateScheduledReport(string accessToken) { httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); // 根据实际API要求构造请求体,此处为示例,需替换为真实请求内容 var requestContent = new StringContent(JsonConvert.SerializeObject(new {}), System.Text.Encoding.UTF8, "application/json"); var response = await httpClient.PostAsync($"{baseUrl}/ScheduledReport", requestContent); response.EnsureSuccessStatusCode(); var responseData = JsonConvert.DeserializeObject<dynamic>(await response.Content.ReadAsStringAsync()); return responseData.reportId; // 根据实际返回结构调整字段名 } public async Task<string> GetReportDownloadUrl(string accessToken, string reportId, int maxRetries = 10, int delaySeconds = 30) { httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); for (int i = 0; i < maxRetries; i++) { var response = await httpClient.GetAsync($"{baseUrl}/ScheduledReport/execution/{reportId}"); response.EnsureSuccessStatusCode(); var responseData = JsonConvert.DeserializeObject<dynamic>(await response.Content.ReadAsStringAsync()); string downloadUrl = responseData.downloadUrl; // 根据实际返回结构调整字段名 if (!string.IsNullOrEmpty(downloadUrl)) { return downloadUrl; } await Task.Delay(TimeSpan.FromSeconds(delaySeconds)); } throw new Exception("达到最大重试次数,未获取到有效下载链接"); } // 新增:根据下载链接下载文件到Azure存储账户 public async Task DownloadFileToStorage(string downloadUrl, string storageConnectionString, string containerName, string fileName) { var blobClient = new Azure.Storage.Blobs.BlobContainerClient(storageConnectionString, containerName); await blobClient.CreateIfNotExistsAsync(); using var httpResponse = await httpClient.GetAsync(downloadUrl); httpResponse.EnsureSuccessStatusCode(); var blob = blobClient.GetBlobClient(fileName); await blob.UploadAsync(httpResponse.Content, overwrite: true); } } }
3. 创建Timer Trigger函数作为每日执行入口
using System; using System.Threading.Tasks; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; namespace PartnerCenterAPI.Properties { public static class DailyReportFunction { [FunctionName("DailyReportFunction")] public static async Task Run( [TimerTrigger("0 0 0 * * *")] TimerInfo myTimer, ILogger log) { log.LogInformation($"Daily report process started at: {DateTime.Now}"); // 从环境变量获取配置 string tenantId = Environment.GetEnvironmentVariable("TenantId", EnvironmentVariableTarget.Process); string clientId = Environment.GetEnvironmentVariable("ClientId", EnvironmentVariableTarget.Process); string clientSecret = Environment.GetEnvironmentVariable("ClientSecret", EnvironmentVariableTarget.Process); string storageConnectionString = Environment.GetEnvironmentVariable("TableStorage", EnvironmentVariableTarget.Process); string blobStorageConnectionString = Environment.GetEnvironmentVariable("BlobStorage", EnvironmentVariableTarget.Process); string blobContainerName = "reports"; // 存储文件的容器名,可配置为环境变量 try { // 1. 读取存储的refresh token var storageClient = new AzureStorageClient(storageConnectionString); // userId需与AuthHttpTrigger中存储的一致,可从环境变量或存储中获取,此处为示例值 string userId = "your-user-id"; var tokenEntity = await storageClient.GetRefreshToken(clientId, userId); if (tokenEntity == null || string.IsNullOrEmpty(tokenEntity.RefreshToken)) { throw new Exception("未找到有效的refresh token,请先执行AuthHttpTrigger获取授权"); } // 2. 使用refresh token获取新的access token和refresh token var identityClient = new MicrosoftIdentityClient(clientId, clientSecret, tenantId); var (newIdToken, newAccessToken, newRefreshToken) = await identityClient.GetAccessTokenFromRefreshToken(tokenEntity.RefreshToken); // 更新存储的refresh token await storageClient.AddOrUpdateRefreshToken(clientId, userId, newRefreshToken); log.LogInformation("Refresh token已更新"); // 3. 创建报告获取reportId var partnerCenterClient = new PartnerCenterApiClient(); string reportId = await partnerCenterClient.CreateScheduledReport(newAccessToken); log.LogInformation($"报告创建成功,reportId: {reportId}"); // 4. 轮询获取下载链接 string downloadUrl = await partnerCenterClient.GetReportDownloadUrl(newAccessToken, reportId); log.LogInformation($"获取到下载链接: {downloadUrl}"); // 5. 下载文件到Blob存储 string fileName = $"report_{DateTime.Now:yyyyMMdd}.csv"; // 根据实际文件类型调整后缀 await partnerCenterClient.DownloadFileToStorage(downloadUrl, blobStorageConnectionString, blobContainerName, fileName); log.LogInformation($"文件已成功下载至存储账户,文件名: {fileName}"); } catch (Exception ex) { log.LogError($"流程执行失败: {ex.Message}"); throw; } log.LogInformation($"Daily report process completed at: {DateTime.Now}"); } } }
内容的提问来源于stack exchange,提问作者Taylor83
相关产品推荐
相关产品推荐

