突破数据瓶颈:MediatR流式请求与异步枚举实战指南
你是否还在为大数据集处理导致的内存溢出而烦恼?是否遇到过实时数据推送场景下响应缓慢的问题?本文将带你掌握MediatR流式请求(Streaming Requests)与异步枚举(Async Enumeration)技术,通过分步实践,让你轻松实现高效的数据流处理,解决传统一次性加载数据的痛点。读完本文,你将学会如何构建流式API、处理实时数据推送,并掌握异步枚举的最佳实践。
流式请求核心概念
MediatR作为.NET平台上的中介者模式实现,通过引入流式请求机制,允许我们以异步方式逐步处理数据,而非一次性加载全部内容。这种方式特别适合处理大数据集、实时数据流以及需要低延迟响应的场景。
核心接口定义
MediatR的流式请求功能基于两个核心接口构建:
- IStreamRequest :标记一个请求为流式请求,TResponse为返回的数据流项类型
- IStreamRequestHandler<TRequest, TResponse>:处理流式请求的处理器接口,返回IAsyncEnumerable
虽然IStreamRequest 接口的源码文件未找到,但我们可以从 src/MediatR/IStreamRequestHandler.cs中看到它的使用:
public interface IStreamRequestHandler<in TRequest, out TResponse>
where TRequest : IStreamRequest<TResponse>
{
IAsyncEnumerable<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
工作原理
MediatR流式请求的工作流程如下:
- 客户端发送实现IStreamRequest 的请求
- 中介者找到对应的IStreamRequestHandler处理器
- 处理器通过IAsyncEnumerable 异步地、逐个地返回结果
- 客户端通过await foreach异步枚举结果,实现流式处理
动手实现流式请求
让我们通过MediatR示例项目中的"Sing"场景,一步步实现流式请求功能。
1. 定义流式请求
首先创建一个实现IStreamRequest 接口的请求类,指定响应类型为Song:
samples/MediatR.Examples/Streams/Sing.cs
public class Sing : IStreamRequest<Song>
{
public string Message { get; set; }
}
2. 定义响应模型
创建Song类作为流中的数据项:
samples/MediatR.Examples/Streams/Song.cs
public class Song
{
public string Message { get; set; }
}
3. 实现流式处理器
创建处理流式请求的处理器,实现IStreamRequestHandler接口:
samples/MediatR.Examples/Streams/SingHandler.cs
public class SingHandler : IStreamRequestHandler<Sing, Song>
{
private readonly TextWriter _writer;
public SingHandler(TextWriter writer)
{
_writer = writer;
}
public async IAsyncEnumerable<Song> Handle(Sing request, [EnumeratorCancellation]CancellationToken cancellationToken)
{
await _writer.WriteLineAsync($"--- Handled Sing: {request.Message}, Song");
yield return await Task.Run(() => new Song { Message = request.Message + "ing do" });
yield return await Task.Run(() => new Song { Message = request.Message + "ing re" });
yield return await Task.Run(() => new Song { Message = request.Message + "ing mi" });
yield return await Task.Run(() => new Song { Message = request.Message + "ing fa" });
yield return await Task.Run(() => new Song { Message = request.Message + "ing so" });
yield return await Task.Run(() => new Song { Message = request.Message + "ing la" });
yield return await Task.Run(() => new Song { Message = request.Message + "ing ti" });
yield return await Task.Run(() => new Song { Message = request.Message + "ing do" });
}
}
在这个处理器中,我们使用了C#的yield return语法,逐个返回Song对象,实现了数据流的异步生成。[EnumeratorCancellation]属性允许在枚举过程中取消操作。
发送和处理流式请求
创建好请求和处理器后,我们需要在应用中发送流式请求并处理结果。
发送流式请求
通过IMediator接口的CreateStream方法发送流式请求:
samples/MediatR.Examples/Runner.cs
await writer.WriteLineAsync("Sending Sing...");
try
{
int i = 0;
await foreach (Song s in mediator.CreateStream(new Sing { Message = "Sing" }))
{
if (i == 0) {
failedSing = !(s.Message.Contains("Singing do"));
}
else if (i == 1)
{
failedSing = !(s.Message.Contains("Singing re"));
}
// ... 其他音高检查
failedSing = failedSing || (++i) > 10;
}
}
catch (Exception e)
{
failedSing = true;
await writer.WriteLineAsync(e.ToString());
}
中介者实现细节
MediatR的Mediator类中实现了CreateStream方法,用于处理流式请求:
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(IStreamRequest<TResponse> request, CancellationToken cancellationToken = default)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
var streamHandler = (StreamRequestHandlerWrapper<TResponse>)_streamRequestHandlers.GetOrAdd(request.GetType(), static requestType =>
{
var wrapperType = typeof(StreamRequestHandlerWrapperImpl<,>).MakeGenericType(requestType, typeof(TResponse));
var wrapper = Activator.CreateInstance(wrapperType) ?? throw new InvalidOperationException($"Could not create wrapper for type {requestType}");
return (StreamRequestHandlerBase)wrapper;
});
var items = streamHandler.Handle(request, _serviceProvider, cancellationToken);
return items;
}
流式请求的高级应用
1. 实时数据推送
流式请求非常适合实现实时数据推送功能,如股票行情更新、聊天消息等。只需将数据生成逻辑替换为实际的数据源监听即可:
public async IAsyncEnumerable<StockPrice> Handle(SubscribeStockPrices request, [EnumeratorCancellation]CancellationToken cancellationToken)
{
var stockTicker = new StockTicker(request.StockSymbol);
// 监听实时股票价格变化
stockTicker.PriceChanged += (sender, e) =>
{
// 推送新价格
yield return new StockPrice { Symbol = request.StockSymbol, Price = e.Price };
};
// 保持连接直到取消
await Task.Delay(Timeout.Infinite, cancellationToken);
}
2. 大数据集分页处理
对于大型数据库查询,可以使用流式请求实现高效的分页处理,避免一次性加载所有数据:
public async IAsyncEnumerable<Product> Handle(SearchProducts request, [EnumeratorCancellation]CancellationToken cancellationToken)
{
int offset = 0;
const int pageSize = 100;
while (true)
{
// 分页查询数据
var products = await _dbContext.Products
.Where(p => p.Name.Contains(request.SearchTerm))
.OrderBy(p => p.Name)
.Skip(offset)
.Take(pageSize)
.ToListAsync(cancellationToken);
if (products.Count == 0)
yield break;
foreach (var product in products)
yield return product;
offset += pageSize;
}
}
3. 流式处理中的异常处理
在流式处理中,异常处理尤为重要。我们可以在处理器中添加try-catch块,或者在管道中添加异常处理行为:
public async IAsyncEnumerable<DataItem> Handle(StreamDataRequest request, [EnumeratorCancellation]CancellationToken cancellationToken)
{
try
{
await foreach (var item in _dataSource.GetItemsAsync(cancellationToken))
{
// 处理数据项
yield return ProcessItem(item);
}
}
catch (OperationCanceledException)
{
// 处理取消操作
yield return new DataItem { Status = "Cancelled" };
}
catch (Exception ex)
{
// 处理错误
yield return new DataItem { Status = "Error", Message = ex.Message };
}
}
最佳实践与注意事项
1. 取消操作
始终支持取消操作,通过CancellationToken及时释放资源:
public async IAsyncEnumerable<TResponse> Handle(TRequest request,
[EnumeratorCancellation]CancellationToken cancellationToken)
{
// 在每个异步操作中传递cancellationToken
await foreach (var item in _source.WithCancellation(cancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
yield return item;
}
}
2. 资源管理
确保及时释放非托管资源,可使用using语句或try-finally块:
public async IAsyncEnumerable<LogEntry> Handle(StreamLogsRequest request,
[EnumeratorCancellation]CancellationToken cancellationToken)
{
using var logReader = new LogFileReader(request.FilePath);
await foreach (var entry in logReader.ReadAsync(cancellationToken))
{
yield return entry;
}
}
3. 背压处理
考虑实现背压(Backpressure)机制,避免生产者速度远快于消费者:
public async IAsyncEnumerable<DataChunk> Handle(LargeDataRequest request,
[EnumeratorCancellation]CancellationToken cancellationToken)
{
var buffer = new Queue<DataChunk>();
var semaphore = new SemaphoreSlim(5); // 限制缓冲区大小
// 生产者任务
_ = Task.Run(async () =>
{
for (int i = 0; i < request.TotalChunks; i++)
{
await semaphore.WaitAsync(cancellationToken);
buffer.Enqueue(await GenerateChunkAsync(i, cancellationToken));
}
}, cancellationToken);
// 消费者枚举
while (buffer.Count > 0 || !_producerCompleted)
{
while (buffer.Count > 0)
{
var chunk = buffer.Dequeue();
semaphore.Release();
yield return chunk;
}
await Task.Delay(100, cancellationToken);
}
}
总结
MediatR的流式请求与异步枚举功能为处理大数据集和实时数据流提供了强大支持。通过实现IStreamRequest和IStreamRequestHandler接口,我们可以轻松构建高效的流式API,避免传统一次性加载数据导致的性能问题。
本文介绍的关键知识点包括:
- 流式请求的核心接口与工作原理
- 如何创建流式请求和处理器
- 发送和处理流式请求的方法
- 流式请求的高级应用场景
- 最佳实践与注意事项
掌握这些知识后,你可以在自己的项目中灵活应用流式请求技术,提升应用性能和用户体验。无论是实时数据推送、大数据集处理还是异步任务监控,MediatR流式请求都能为你提供优雅而高效的解决方案。
希望本文对你有所帮助,如果你有任何问题或建议,请在评论区留言讨论。记得点赞、收藏本文,关注作者获取更多.NET开发技巧!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



