求助:将分页API异步请求任务改造为泛型实现
问题
我正在开发一款内部控制台应用,用于从分页JSON API获取全量结果。目前针对17种不同数据类型编写了重复的异步请求代码,希望将其改造为泛型实现,但在访问二级数据对象(如Ticket)和nextPageURL属性时遇到问题,同时希望保留回调结构以在控制台实时输出处理进度。现有代码及初步尝试的泛型代码如下:
现有实体类
public class Tickets { public Ticket[] tickets { get; set; } public string nextPageURL { get; set; } public string previousPageURL { get; set; } public int recordCount { get; set; } } public class Ticket { ... }
非泛型异步任务代码
private static async Task<List<Ticket>> GetTicketsAsync(Action<Tickets> callBack = null) { var tickets = new List<Ticket>(); HttpClient httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes($"-----"))); httpClient.BaseAddress = new Uri("-----")); var nextUrl = "/api/tickets"; do { await httpClient.GetAsync(nextUrl) .ContinueWith(async (ticketSearchTask) => { var response = await ticketSearchTask; if (response.IsSuccessStatusCode) { string jsonString = await response.Content.ReadAsStringAsync(); try { var result = JsonSerializer.Deserialize<Tickets>(jsonString); if (result != null) { // Build the full list to return later after the loop. if (result.tickets.Any()) tickets.AddRange(result.tickets.ToList()); // Run the callback method, passing the current page of data from the API. if (callBack != null) callBack(result); // Get the URL for the next page nextUrl = (result.nextPageURL != null) ? result.nextPageURL : string.Empty; } } catch (Exception ex) { Console.WriteLine($"\n We ran into an error: {ex.Message}"); nextUrl = string.Empty; } } else { // End loop if we get an error response. nextUrl = string.Empty; } }); } while (!string.IsNullOrEmpty(nextUrl)); return tickets; } private static void TicketsCallBack(Tickets tickets) { if (tickets != null && tickets.recordCount > 0) { foreach (var ticket in tickets.tickets) { Console.WriteLine($"fetched ticket: {ticket.id}"); } } }
初步尝试的泛型代码
private static async Task<List<T>> GetAsync<T>(string type, Action<T> callBack = null, string ticketId = null) { var results = new List<T>(); HttpClient httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes($"-----"))); httpClient.BaseAddress = new Uri("-----")); var nextUrl = $"/api/{type}.json"; do { await httpClient.GetAsync(nextUrl) .ContinueWith(async (searchTask) => { var response = await searchTask; if (response.IsSuccessStatusCode) { string jsonString = await response.Content.ReadAsStringAsync(); try { var result = JsonSerializer.Deserialize<T>(jsonString); if (result != null) { // Build the full list to return later after the loop. if (result.tickets.Any()) results.AddRange(result.tickets.ToList()); // Run the callback method, passing the current page of data from the API. if (callBack != null) callBack(result); // Get the URL for the next page nextUrl = (result.GetType().GetProperty("nextPageURL") != null) ? result.GetType().GetProperty("nextPageURL").ToString() : string.Empty; } } catch (Exception ex) { Console.WriteLine($"\nWe ran into an error: {ex.Message}"); nextUrl = string.Empty; } } else { // End loop if we get an error response. nextUrl = string.Empty; } }); } while (!string.IsNullOrEmpty(nextUrl)); return results; }
解决方案
1. 定义通用分页响应接口
给所有分页响应类统一约束,让泛型方法能类型安全地访问数据集合和分页属性:
public interface IPaginatedResponse<TItem> { TItem[] Items { get; set; } string nextPageURL { get; set; } int recordCount { get; set; } }
2. 修改实体类实现接口
以Tickets为例,通过显式实现接口映射现有属性,不需要修改原有字段名:
public class Tickets : IPaginatedResponse<Ticket> { public Ticket[] tickets { get; set; } // 显式实现接口,关联到现有tickets属性 Ticket[] IPaginatedResponse<Ticket>.Items { get => tickets; set => tickets = value; } public string nextPageURL { get; set; } public string previousPageURL { get; set; } public int recordCount { get; set; } }
如果是新开发的实体类,可以直接把集合属性命名为Items,省去显式实现的步骤。
3. 实现类型安全的泛型方法
利用接口约束替代反射,同时优化异步写法(去掉ContinueWith,直接用await符合最佳实践):
private static async Task<List<TItem>> GetPaginatedDataAsync<TResponse, TItem>(string initialUrl, Action<TResponse> callback = null) where TResponse : IPaginatedResponse<TItem>, new() { var fullResults = new List<TItem>(); // 建议全局复用HttpClient,避免频繁创建销毁 using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes($"-----"))); httpClient.BaseAddress = new Uri("-----"); string nextUrl = initialUrl; while (!string.IsNullOrEmpty(nextUrl)) { try { var response = await httpClient.GetAsync(nextUrl); if (!response.IsSuccessStatusCode) { Console.WriteLine($"请求失败,状态码:{response.StatusCode}"); break; } string jsonString = await response.Content.ReadAsStringAsync(); var pageResult = JsonSerializer.Deserialize<TResponse>(jsonString); if (pageResult == null) { Console.WriteLine("解析响应为空"); break; } // 添加当前页数据到全量列表 if (pageResult.Items != null && pageResult.Items.Any()) { fullResults.AddRange(pageResult.Items); } // 执行回调,实时输出进度 callback?.Invoke(pageResult); // 更新下一页URL nextUrl = pageResult.nextPageURL; } catch (Exception ex) { Console.WriteLine($"\n请求出错:{ex.Message}"); break; } } return fullResults; }
4. 调用泛型方法示例
原有回调方法不需要修改,直接传入泛型方法即可:
// 获取全量Ticket数据 var allTickets = await GetPaginatedDataAsync<Tickets, Ticket>("/api/tickets", TicketsCallBack); // 原有回调方法保持不变 private static void TicketsCallBack(Tickets tickets) { if (tickets != null && tickets.recordCount > 0) { foreach (var ticket in tickets.tickets) { Console.WriteLine($"fetched ticket: {ticket.id}"); } } }
5. 扩展:简化API路径调用
如果所有数据类型的API路径遵循/api/{type}的规则,可以再封装一层:
private static async Task<List<TItem>> GetDataByTypeAsync<TResponse, TItem>(string type, Action<TResponse> callback = null) where TResponse : IPaginatedResponse<TItem>, new() { return await GetPaginatedDataAsync<TResponse, TItem>($"/api/{type}", callback); } // 调用示例 var allTickets = await GetDataByTypeAsync<Tickets, Ticket>("tickets", TicketsCallBack);
内容的提问来源于stack exchange,提问作者Tony Law
相关产品推荐
相关产品推荐

