ASP.NET Core:为过滤后的IQueryable列表填充OData非实体属性
问题
我有一个运行正常的OData API,能正确返回所有领域对象的响应。现在遇到的问题是如何为对象填充非实体类型的值。
我有一组InventoryItem对象,每个对象包含ReadingTags列表,但将其转换为DTO列表后遍历ReadingTags时,由于数据执行被延迟到响应序列化时,获取到的是全部InventoryItem列表。我需要先把第三方服务的响应合并到DTO中,再返回给用户。
参考代码如下:
领域实体
public class InventoryItem { public virtual Guid Id { get; set; } public virtual string Name { get; set; } public virtual ISet<ReadingTag> ReadingTags { get; set; } = new HashSet<ReadingTag>(); } public class ReadingTag { public virtual Guid Id { get; set; } public virtual string DeviceName { get; set; } // 不能把这个属性加到领域模型里,因为它不是数据库/实体属性 //public IList<double> Readings { get; set; } = new List<double>(); }
DTO实体
public class InventoryItemDto { public Guid Id { get; set; } public string Name { get; set; } public IList<double> Readings { get; set; } = new List<double>(); }
控制器
[HttpGet] [EnableQuery] [ProducesResponseType(200)] public ActionResult Get() { var query = applicationService.All(); var results = _mapper.Map<List<InventoryItemDto>>(query); // Automapper return Ok(results); }
服务
public Task<IQueryable<InventoryItem>> All() { var inventoryItems = _session.Query<InventoryItem>(); // 我希望能把过滤后的响应发送到另一个服务,填充ReadingTags的Readings return inventoryItems; }
解决方案
核心问题是OData的EnableQuery会延迟查询执行,导致无法在映射DTO后立即拿到过滤后的数据集去调用第三方服务。以下是几种可行的实现方式:
方式一:手动触发查询执行,再填充第三方数据
修改控制器逻辑,先让OData过滤器生效并执行查询,拿到具体的实体列表后再映射DTO、调用第三方服务填充数据:
[HttpGet] [EnableQuery] [ProducesResponseType(200)] public async Task<ActionResult> Get(ODataQueryOptions<InventoryItem> options) { // 获取原始查询 var query = await applicationService.All(); // 应用OData过滤条件,触发查询执行得到实体列表 var filteredItems = await options.ApplyTo(query).Cast<InventoryItem>().ToListAsync(); // 映射到DTO var dtos = _mapper.Map<List<InventoryItemDto>>(filteredItems); // 收集需要调用第三方服务的标识(比如所有ReadingTag的Id) var tagIds = filteredItems.SelectMany(item => item.ReadingTags).Select(tag => tag.Id).Distinct().ToList(); // 调用第三方服务批量获取Readings数据 var tagReadingsMap = await thirdPartyService.GetReadingsByTagIdsAsync(tagIds); // 填充DTO的Readings属性 foreach (var dto in dtos) { var item = filteredItems.First(i => i.Id == dto.Id); dto.Readings = item.ReadingTags .SelectMany(tag => tagReadingsMap.TryGetValue(tag.Id, out var readings) ? readings : new List<double>()) .ToList(); } return Ok(dtos); }
方式二:使用AutoMapper的自定义映射逻辑
在AutoMapper的配置中添加自定义映射,结合延迟加载的处理,但需要确保查询已经执行:
首先配置AutoMapper:
CreateMap<InventoryItem, InventoryItemDto>() .ForMember(dest => dest.Readings, opt => opt.MapFrom(async (src, dest, context) => { // 确保src.ReadingTags已经被加载(可以用Include或者显式加载) var tagIds = src.ReadingTags.Select(t => t.Id).Distinct().ToList(); var readingsMap = await context.GetService<IThirdPartyService>().GetReadingsByTagIdsAsync(tagIds); return src.ReadingTags.SelectMany(t => readingsMap.TryGetValue(t.Id, out var r) ? r : new List<double>()).ToList(); }));
然后修改控制器,确保查询执行后再映射:
[HttpGet] [EnableQuery] [ProducesResponseType(200)] public async Task<ActionResult> Get(ODataQueryOptions<InventoryItem> options) { var query = await applicationService.All(); // 应用OData过滤并执行查询,同时Include ReadingTags避免延迟加载 var filteredItems = await options.ApplyTo(query.Include(i => i.ReadingTags)) .Cast<InventoryItem>() .ToListAsync(); // 使用AutoMapper的异步映射 var dtos = await _mapper.ProjectTo<InventoryItemDto>(filteredItems.AsQueryable()).ToListAsync(); return Ok(dtos); }
方式三:利用OData的PostProcessing过滤器
如果不想修改控制器逻辑,可以自定义OData的PostProcessing过滤器,在查询执行后、序列化前填充数据:
- 实现
IODataQueryableInterceptor:
public class ReadingTagsPostProcessor : IODataQueryableInterceptor { private readonly IThirdPartyService _thirdPartyService; private readonly IMapper _mapper; public ReadingTagsPostProcessor(IThirdPartyService thirdPartyService, IMapper mapper) { _thirdPartyService = thirdPartyService; _mapper = mapper; } public async ValueTask<object> QueryExecutedAsync(object queryResult, Type elementType, ODataQueryContext context, CancellationToken cancellationToken) { if (elementType == typeof(InventoryItem) && queryResult is IEnumerable<InventoryItem> items) { var itemList = items.ToList(); var tagIds = itemList.SelectMany(i => i.ReadingTags).Select(t => t.Id).Distinct().ToList(); var readingsMap = await _thirdPartyService.GetReadingsByTagIdsAsync(tagIds); var dtos = _mapper.Map<List<InventoryItemDto>>(itemList); foreach (var dto in dtos) { var item = itemList.First(i => i.Id == dto.Id); dto.Readings = item.ReadingTags .SelectMany(t => readingsMap.TryGetValue(t.Id, out var r) ? r : new List<double>()) .ToList(); } return dtos; } return queryResult; } }
- 注册过滤器:
在Startup/Program.cs中添加:
services.AddScoped<IODataQueryableInterceptor, ReadingTagsPostProcessor>();
- 控制器可以保持原有逻辑,但需要确保查询会被执行:
[HttpGet] [EnableQuery] [ProducesResponseType(200)] public async Task<ActionResult> Get() { var query = await applicationService.All(); // 这里不需要手动映射,过滤器会处理 return Ok(query); }
关键注意事项
- 避免N+1问题:一定要批量调用第三方服务,不要在循环里逐个请求,否则性能会严重下降。
- 延迟加载处理:确保
ReadingTags被提前加载(用Include或者显式加载),否则在访问时可能会触发额外的数据库查询。 - OData查询的执行时机:必须确保OData的过滤、分页等条件已经应用并执行查询后,再去调用第三方服务,否则会拿到全部数据,失去OData的过滤意义。
内容的提问来源于stack exchange,提问作者Mart_
相关产品推荐
相关产品推荐

