基于Graph API PageIterator实现GetAsync()泛型扩展方法的类型兼容问题及流畅调用需求
我之前做Graph API开发时也碰到过这个头疼的问题——Task的泛型不变性确实会破坏链式调用的流畅感。不过咱们可以通过两层泛型扩展方法来完美解决这个问题,既实现代码复用,又保留你想要的GetAsync().GetAll()调用风格。
解决方案代码
首先,先实现针对ICollectionPage<TItem>的核心迭代逻辑(这部分做了参数校验和功能扩展):
using Microsoft.Graph; using System.Collections.Generic; using System.Threading.Tasks; public static class GraphCollectionExtensions { // 核心迭代方法,封装PageIterator的逻辑 private static async Task<IReadOnlyList<TItem>> GetAllInternal<TItem>(this ICollectionPage<TItem> page, IBaseClient baseClient, int? maxItems = null) { if (page == null) throw new ArgumentNullException(nameof(page)); if (baseClient == null) throw new ArgumentNullException(nameof(baseClient)); if (maxItems.HasValue && maxItems.Value <= 0) throw new ArgumentOutOfRangeException(nameof(maxItems)); var allElements = new List<TItem>(); var iterator = PageIterator<TItem>.CreatePageIterator( baseClient, page, item => { allElements.Add(item); // 支持最大条目数限制,达到后停止迭代 return !maxItems.HasValue || allElements.Count < maxItems.Value; }); await iterator.IterateAsync().ConfigureAwait(false); return allElements; } // 针对Task<TPage>的扩展方法,支持链式调用,编译器自动推断泛型参数 public static async Task<IReadOnlyList<TItem>> GetAll<TItem, TPage>(this Task<TPage> pageTask, IBaseClient baseClient, int? maxItems = null) where TPage : ICollectionPage<TItem> { if (pageTask == null) throw new ArgumentNullException(nameof(pageTask)); var page = await pageTask.ConfigureAwait(false); return await page.GetAllInternal(baseClient, maxItems).ConfigureAwait(false); } // 重载版本,进一步简化调用,无需手动指定TPage类型 public static async Task<IReadOnlyList<TItem>> GetAll<TItem>(this Task<ICollectionPage<TItem>> pageTask, IBaseClient baseClient, int? maxItems = null) { if (pageTask == null) throw new ArgumentNullException(nameof(pageTask)); var page = await pageTask.ConfigureAwait(false); return await page.GetAllInternal(baseClient, maxItems).ConfigureAwait(false); } }
调用示例
现在你可以像最开始期望的那样流畅链式调用了,编译器会自动推断泛型参数:
// 基础用法:获取所有成员 var currentGroupMembers = await serviceClient.Groups[azureGroup.Id].Members.Request() .GetAsync() .GetAll<DirectoryObject>(serviceClient); // 带最大条目数限制的用法 var first100Members = await serviceClient.Groups[azureGroup.Id].Members.Request() .GetAsync() .GetAll<DirectoryObject>(serviceClient, maxItems: 100); // 甚至可以省略TItem的指定(如果上下文可以推断的话) IReadOnlyList<DirectoryObject> members = await serviceClient.Groups[azureGroup.Id].Members.Request() .GetAsync() .GetAll(serviceClient);
为什么这个方案能解决问题?
问题的核心在于:.NET的Task<T>是不变泛型类型——哪怕IGroupMembersCollectionWithReferencesPage实现了ICollectionPage<DirectoryObject>,Task<IGroupMembersCollectionWithReferencesPage>也不能直接转换为Task<ICollectionPage<DirectoryObject>>。
通过添加带有where TPage : ICollectionPage<TItem>泛型约束的Task<TPage>扩展方法,我们让编译器识别出具体的TPage类型确实是ICollectionPage<TItem>的实现。在方法内部await得到TPage实例后,再调用针对ICollectionPage<TItem>的核心逻辑,完美绕开了Task的协变限制。
额外优势
- 完全复用代码:不用再为每个
...Collection...Page类型单独写重复的迭代逻辑 - 保留链式调用:和你最初的非泛型方法调用体验完全一致
- 支持额外参数:比如添加了
maxItems参数,方便限制返回的最大条目数 - 完善的错误处理:加入了参数校验,避免空引用等低级错误
内容的提问来源于stack exchange,提问作者Oliver
相关产品推荐
相关产品推荐

