什么是 IAsyncEnumerable 呢?
IAsyncEnumerable<T> 是 .NET 中用于表示异步数据流的接口。
它允许你逐个异步地获取数据项,而不是将所有数据一次性加载到内存中。这样可以减少内存占用,尤其在处理大量数据时更加高效。
与 IEnumerable<T> 不同,IEnumerable<T> 是同步的,要求所有数据在返回之前就加载完成。
而 IAsyncEnumerable<T> 是异步的,支持在数据流被请求时逐步加载,适合处理从数据库或网络等源异步获取的数据。
示例:
public async IAsyncEnumerable<int> GetNumbersAsync()
{
    for (int i = 0; i < 10; i++)
    {
        await Task.Delay(100);  // 模拟异步操作
        yield return i;
    }
}
好处
减少内存占用:IAsyncEnumerable<T> 逐步加载数据,避免了需要将所有数据一次性加载到内存中。对于大数据量的查询,能显著减少内存压力。
提升响应性能:在 WebAPI 中,返回 IAsyncEnumerable<T> 可以让客户端在获取部分数据时立即开始处理,而无需等待所有数据都加载完成。这使得响应时间更短,提升用户体验。
避免阻塞操作:使用 async/await 使得 WebAPI 不会被同步阻塞操作所拖慢,能够更好地处理并发请求。
以 WebAPI + EFCore 举例
假设我们需要通过 Entity Framework Core 从数据库中查询大量记录。
如果我们一次性加载所有数据,可能会导致内存占用过高,甚至影响性能。使用 IAsyncEnumerable<T>,我们可以逐个获取数据。
示例代码:
public class ProductController : ControllerBase
{
    private readonly ApplicationDbContext _context;
    public ProductController(ApplicationDbContext context)
    {
        _context = context;
    }
    [HttpGet("products")]
    public async IAsyncEnumerable<Product> GetProductsAsync()
    {
        await foreach (var product in _context.Products.AsAsyncEnumerable())
        {
            yield return product;
        }
    }
}
在这个例子中,AsAsyncEnumerable() 方法将 DbSet<Product> 转换成一个异步数据流,await foreach 循环逐个从数据库中异步获取数据并返回,避免了内存占用过多。
以 WebAPI + HTTPClient 举例
在 WebAPI 中,你可能需要调用其他服务或外部 API 来获取数据。使用 IAsyncEnumerable<T> 可以使得调用返回的数据逐步加载,避免等待整个请求完成后再返回。
示例代码:
public class ExternalApiController : ControllerBase
{
    private readonly HttpClient _httpClient;
    public ExternalApiController(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    [HttpGet("external-data")]
    public async IAsyncEnumerable<string> GetExternalDataAsync()
    {
        var response = await _httpClient.GetAsync("https://api.example.com/data");
        response.EnsureSuccessStatusCode();
        var stream = await response.Content.ReadAsStreamAsync();
        using (var reader = new StreamReader(stream))
        {
            string? line;
            while ((line = await reader.ReadLineAsync()) != null)
            {
                yield return line;
            }
        }
    }
}
在这个例子中,我们通过 HTTPClient 请求外部 API 数据,并使用 IAsyncEnumerable<string> 返回每一行数据,允许客户端逐步处理数据,而无需等待所有数据都加载完毕。
客户端怎么处理 IAsyncEnumerable<T>
客户端接收到 IAsyncEnumerable<T> 数据流后,可以使用异步迭代器 await foreach 来逐步处理数据。
这样可以在数据流逐步传输过程中及时处理和显示数据,而不必等待全部数据加载完成。
C# 客户端代码:
public class ProductClient
{
    private readonly HttpClient _httpClient;
    public ProductClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    public async Task GetProductsAsync()
    {
        var response = await _httpClient.GetAsync("https://example.com/products");
        response.EnsureSuccessStatusCode();
        await foreach (var product in response.Content.ReadFromJsonAsync<IAsyncEnumerable<Product>>())
        {
            Console.WriteLine(product.Name);
        }
    }
}
JavaScript 客户端处理:
在 JavaScript 中,客户端可以使用 fetch API 和流(Streams)来逐步处理数据。WebAPI 返回的数据流可以通过 Response.body.getReader() 来读取并逐步消费。
async function fetchProducts() {
    const response = await fetch('https://example.com/products');
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let done = false;
    let value = '';
    while (!done) {
        const { done: chunkDone, value: chunk } = await reader.read();
        done = chunkDone;
        value += decoder.decode(chunk, { stream: true });
        console.log(value);  // 逐步输出数据
    }
}
通过上述方式,JavaScript 客户端可以逐步处理 WebAPI 返回的异步流,提升用户体验和响应速度。
.NET优雅的使用Patch: JsonPatch
引言
在现代 Web API 开发中,我们经常需要对资源进行部分更新(Partial Update)。传统的 PUT 请求会要求发送整个对象,而 PATCH 请求可以仅发送需要更新的字段。ASP.NET Core 提供了 JsonPatchDocument<T> 来简化这一操作。
什么是 JsonPatch?
JsonPatch(基于 RFC 6902)是一种 JSON 格式的补丁文档,允许客户端声明式地修改 JSON 资源。JsonPatch 提供了以下操作:
add:添加一个新值
remove:删除一个字段
replace:替换一个字段的值
move:移动一个值
copy:复制一个值
test:测试一个值是否符合预期
在 ASP.NET Core Web API 中使用 JsonPatch
安装依赖
JsonPatch 已内置于 Microsoft.AspNetCore.Mvc.NewtonsoftJson,你需要确保你的项目引用了该包:
builder.Services.AddControllers().AddNewtonsoftJson();
创建 API 控制器
假设我们有一个 Product 类:
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}
然后,创建 ProductsController 处理 PATCH 请求:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private static List<Product> _products = new()
    {
        new Product { Id = 1, Name = "Laptop", Price = 1200 },
        new Product { Id = 2, Name = "Mouse", Price = 25 }
    };
    [HttpPatch("{id}")]
    public IActionResult Patch(int id, [FromBody] JsonPatchDocument<Product> patchDoc)
    {
        var product = _products.FirstOrDefault(p => p.Id == id);
        if (product == null)
        {
            return NotFound();
        }
        patchDoc.ApplyTo(product, ModelState);
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        return Ok(product);
    }
}
发送 JsonPatch 请求
客户端可以发送如下 PATCH 请求:
[
    { "op": "replace", "path": "/price", "value": 999.99 }
]
示例 cURL 请求:
curl -X PATCH "http://localhost:5000/api/products/1" \
     -H "Content-Type: application/json" \
     -d '[{"op": "replace", "path": "/price", "value": 999.99}]'
处理 JsonPatch 可能遇到的问题
确保 JSON 格式正确
JsonPatch 语法容易出错,比如路径格式不正确或缺少 op。建议使用 Postman 或 cURL 进行调试。
处理 ModelState 错误
如果 patchDoc.ApplyTo(product, ModelState); 返回错误,应该返回 BadRequest(ModelState) 并提供详细的错误信息。
结论
JsonPatch 提供了一种优雅的方式来进行部分更新,避免了 PUT 需要传输整个对象的冗余。合理使用 JsonPatch,可以提高 API 的灵活性和效率。