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

求助:将分页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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 16:55:54