升级LINQ管道至异步时遇编译错误,求规范异步实现方案
规范异步实现优化方案
嘿,我来帮你把这段代码改成符合C#异步最佳实践的版本,先说说你之前遇到的核心问题:
- 一开始
GetSubsciptionOffers里用了async的Select,直接导致返回的是IEnumerable<Task<Product>>,不是你要的IEnumerable<Product> - 后来你用同步阻塞的
GetAwaiter().GetResult()解决,这种写法在ASP.NET这类有同步上下文的环境里很容易触发死锁,完全浪费了异步编程的优势
下面是优化后的完整实现,咱们一步步来:
1. 重构订阅产品的异步方法
先把GetSubsciptionOffers改成真正的异步方法,用批量异步处理替代同步阻塞:
private async Task<IEnumerable<Product>> GetSubscriptionOffersAsync(IEnumerable<StripeProduct> productList) { // 给每个产品创建一个获取价格的异步任务 var productTasks = productList.Select(async product => new Product { Name = product.Name, Id = product.Id, Image = new Uri(product.Images.First()), Description = product.Description, OfferType = OfferTypeEnum.Pro, Price = (await _planRepo.GetPlanByIdAsync(product.Metadata.First().Value)).Amount.GetValueOrDefault() }); // 等待所有异步任务完成,直接拿到Product集合 return await Task.WhenAll(productTasks).ConfigureAwait(false); }
这里用Task.WhenAll批量处理所有异步请求,比循环逐个await效率高很多,而且全程都是异步非阻塞的,不会卡线程。
2. 优化主方法的异步流程
接着修改GetStripeProductsDto,正确处理异步任务的等待和集合合并:
public async Task<IEnumerable<ProductDto>> GetStripeProductsDto(bool isLogged, bool isSubscriber) { // 先异步获取完整的产品列表 var productList = (await _productRepo.GetAllStripeProductsAsync()).ToList(); // 同时启动SKU产品(同步)和订阅产品(异步)的处理任务 var skuOffersTask = Task.FromResult(GetSkuOffers(productList)); var subscriptionOffersTask = GetSubscriptionOffersAsync(productList); // 等待两个任务都完成,拿到结果 var skuOffers = await skuOffersTask.ConfigureAwait(false); var subscriptionOffers = await subscriptionOffersTask.ConfigureAwait(false); // 合并集合、分组、映射成DTO,和你原来的逻辑一致 return skuOffers .Concat(subscriptionOffers) .GroupBy(product => product.Name) .Select(productGroup => new ProductDto { Name = productGroup.Key, Id = productGroup.Select(p => p.Id).First(), Description = productGroup.Select(p => p.Description).First(), Image = productGroup.Select(p => p.Image).First(), CurrentUserProfile = isSubscriber ? OfferTypeEnum.Pro.ToString() : isLogged ? OfferTypeEnum.Registered.ToString() : OfferTypeEnum.Basic.ToString(), Prices = productGroup.Select(p => new { Offer = p.OfferType.ToString(), Price = p.Price.ToString() }) .ToDictionary(p => p.Offer, p => p.Price) }) .ToList(); }
这里的关键细节:
- 用
Task.FromResult把同步的GetSkuOffers包装成任务,这样能和异步任务并行处理(同步方法本身不会并行,但写法统一,不影响性能) - 全程用
ConfigureAwait(false)避免捕获同步上下文,大幅降低死锁概率,这在ASP.NET环境里特别重要 - 去掉了没必要的
Task.Run,因为你调用的是Stripe API这类IO异步操作,不需要额外开线程池线程,避免了多余的线程切换
3. 保留原有的SKU同步方法
你的GetSkuOffers是纯内存操作,不需要异步,直接保留就行:
private IEnumerable<Product> GetSkuOffers(IEnumerable<StripeProduct> productList) { return productList .SelectMany(product => product.Skus.Data, (product, sku) => new Product { Name = product.Name, Id = product.Id, Image = new Uri(product.Images.First()), Description = product.Description, OfferType = sku.Id.Contains("Basic") ? OfferTypeEnum.Basic : OfferTypeEnum.Registered, Price = sku.Price }); }
最后提几个小建议
- 检查
product.Images和product.Metadata是否可能为空,避免First()抛出空引用异常,可以改成FirstOrDefault()再加判断 - 确保
_planRepo.GetPlanByIdAsync是真正的异步IO方法(调用Stripe API的话应该没问题),这样整个流程才是真正的异步非阻塞
内容的提问来源于stack exchange,提问作者Xav Sc
相关产品推荐
相关产品推荐

