基于Microsoft Graph提取Bookings联系人OData ID实现批量清理
业务实现背景
- 日常使用Bookings服务承接联系人预约流程时,系统会自动将相关联系人信息同步存储到无需使用的Bookings客户数据库,需要定期清理这类冗余数据
- 当前开发进度:已完成带身份令牌的GET/DELETE请求封装、身份认证流程打通,暂未实现从GET接口返回的OData格式结果中提取客户ID、循环发起删除请求的逻辑
- 现有基础配置:
- 拉取客户列表的GET端点:
https://graph.microsoft.com/beta/bookingBusinesses/MallLocation@acme.ca/customers/?$select=id&$top=1 - 已封装的两个请求方法:
GetHttpContentWithToken(携带Bearer令牌发送GET请求)、DeleteHttpContentWithToken(携带Bearer令牌发送DELETE请求)
- 拉取客户列表的GET端点:
核心实现步骤
步骤1:定义OData返回的序列化模型
不要通过正则表达式硬抠返回字符串里的ID,Graph返回的OData结构是固定的,直接用强类型反序列化可以避免转义字符、格式变动导致的解析错误,需要提前定义两个对应的数据结构:
/// <summary> /// Graph OData列表返回的通用基础结构 /// </summary> public class GraphODataListResponse<T> { /// <summary> /// 列表条目存在value字段中 /// </summary> public List<T> Value { get; set; } /// <summary> /// 分页下一页地址,对应OData原生字段@odata.nextLink /// </summary> [System.Text.Json.Serialization.JsonPropertyName("@odata.nextLink")] public string OdataNextLink { get; set; } } /// <summary> /// 仅拉取id字段的Bookings客户条目结构 /// </summary> public class BookingCustomerEntry { public string Id { get; set; } }
步骤2:构造单个客户的DELETE请求地址
Graph删除单个Bookings客户的端点规则为业务主体地址拼接对应客户ID,格式如下:
删除端点基础格式:
https://graph.microsoft.com/beta/bookingBusinesses/{对应Bookings业务标识}/customers/{目标客户ID}
举例:如果从列表中拿到的客户ID为9f8e7d6c-5b4a,对应的DELETE地址就是把该ID拼接在customers路径末尾,不需要携带额外查询参数。
步骤3:实现循环遍历删除逻辑
循环逻辑需要覆盖两个场景:一是遍历当前返回页的所有客户ID逐个发送删除请求,二是处理分页拉取直到没有更多待删数据。这里要注意避免触发Graph接口的限流规则,不要高并发批量发请求,单线程顺序删除即可。
把之前认证完成后的调用逻辑替换为如下实现:
if (authResult != null) { // 正式清理时将测试用的$top=1调整为单页最大支持的$top=999,减少请求次数 var currentListEndpoint = graphAPIEndpoint.Replace("$top=1", "$top=999"); bool hasRemainingData = true; while (hasRemainingData) { // 拉取当前分页的客户列表 var odataResponseContent = await GetHttpContentWithToken(currentListEndpoint, authResult.AccessToken); // 反序列化拿到当前页的客户ID列表 var listResult = System.Text.Json.JsonSerializer.Deserialize<GraphODataListResponse<BookingCustomerEntry>>(odataResponseContent); // 边界判断:如果当前页没有可删数据,直接退出循环 if (listResult?.Value == null || listResult.Value.Count == 0) { hasRemainingData = false; break; } // 遍历当前页每个客户ID,逐个发起删除 foreach (var customer in listResult.Value) { // 构造对应客户的DELETE请求地址 var deleteTargetEndpoint = $"https://graph.microsoft.com/beta/bookingBusinesses/MallLocation@acme.ca/customers/{customer.Id}"; // 发起删除请求,可在此处记录删除结果日志 var deleteProcessResult = await DeleteHttpContentWithToken(deleteTargetEndpoint, authResult.AccessToken); } // 处理分页:如果返回结果中存在下一页地址,更新请求地址后继续循环 if (!string.IsNullOrEmpty(listResult.OdataNextLink)) { currentListEndpoint = listResult.OdataNextLink; } else { hasRemainingData = false; } } // 清理完成后更新界面展示 ResultText.Text = "冗余Bookings客户数据清理完成"; DisplayBasicTokenInfo(authResult); this.SignOutButton.Visibility = Visibility.Visible; }
常见踩坑提醒
- 原有代码配置的权限范围
user.read不足以调用Bookings客户的读写接口,需要补充对应的Bookings管理权限(如Bookings.Manage.All),否则请求会返回403权限不足 - 长时间运行清理任务时,不要在每次请求中实例化新的
HttpClient,容易占满系统套接字资源,建议将HttpClient改为单例复用 - 单个客户删除失败时要加异常捕获,不要让局部错误中断整个清理流程,可将失败的ID单独记录后后续重试
内容的提问来源于stack exchange,提问作者Mark Smed
相关产品推荐
相关产品推荐

