asp.net core异步编程最佳实践,ASP.NET Core是一个跨平台的高性能开放源代码框架,用于构建现代的,基于云的,Internet连接的应用程序。本指南捕获了编写可伸缩服务器应用程序时的一些常见陷阱和实践。
asp.net core异步编程最佳实践
继上一篇C#异步编程最佳实践发布之后,本文来讨论一下ASP.NET Core异步编程实践的一些实用指南,本文来自于github的英文翻译,喜欢阅读原文的可以点击这里。
避免在HttpRequest.Body和HttpResponse.Body上使用同步读/写重载
英文原意是:Avoid using synchronous Read/Write overloads on HttpRequest.Body and HttpResponse.Body
ASP.NET Core中的所有IO都是异步的。 服务器实现具有同步和异步重载的Stream接口。 应该首选异步线程,以避免阻塞线程池线程(这可能导致线程池饥饿)
❌ 错误实践 本示例使用StreamReader.ReadToEnd,结果阻塞了当前线程以等待结果。 这是异步中同步的示例。
public class MyController : Controller { [HttpGet("/pokemon")] public ActionResult<PokemonData> Get() { // This synchronously reads the entire http request body into memory. // If the client is slowly uploading, we're doing sync over async because Kestrel does *NOT* support synchronous reads. var json = new StreamReader(Request.Body).ReadToEnd(); return JsonConvert.DeserializeObject<PokemonData>(json); } }
✅ 最佳实践 本示例使用StreamReader.ReadToEndAsync,因此在读取时不会阻塞线程。
public class MyController : Controller { [HttpGet("/pokemon")] public async Task<ActionResult<PokemonData>> Get() { // This asynchronously reads the entire http request body into memory. var json = await new StreamReader(Request.Body).ReadToEndAsync(); return JsonConvert.DeserializeObject<PokemonData>(json); } }
💡注意:如果请求量很大,可能会导致内存不足问题,从而导致拒绝服务。
优先使用HttpRequest.ReadAsFormAsync()而不是HttpRequest.Form
与HttpRequest.Form相比,您应该始终偏爱HttpRequest.ReadAsFormAsync()。 唯一可以安全使用HttpRequest.Form的情况是,对HttpRequest.ReadAsFormAsync()的调用已经读取了表单,并且正在使用HttpRequest.Form读取缓存的表单值。
❌ 错误实践:此示例使用HttpRequest.Form在幕后使用异步之上的同步,并且可能导致线程池不足(在某些情况下)。
public class MyController : Controller { [HttpPost("/form-body")] public IActionResult Post() { var form = HttpRequest.Form; Process(form["id"], form["name"]); return Accepted(); } }
✅ 最佳实践:此示例使用Http Request.ReadFormAsync()异步读取表单主体。
public class MyController : Controller { [HttpPost("/form-body")] public async Task<IActionResult> Post() { var form = await HttpRequest.ReadAsFormAsync(); Process(form["id"], form["name"]); return Accepted(); } }
避免将大的请求正文或响应正文读入内存
在.NET中,大于85KB的任何单个对象分配最终都会出现在大对象堆(LOH)中。大对象在两种方面很昂贵:
- 分配成本很高,因为必须清除新分配的大对象的内存(CLR保证清除所有新分配的对象的内存)
- LOH与其余堆一起收集(它需要“完整垃圾收集”或Gen2收集)
这篇博客文章简要地描述了这个问题:
分配大对象时,将其标记为Gen 2对象。对于小物体,不是Gen 0。结果是,如果LOH中的内存不足,GC不仅会清理LOH,还会清理整个托管堆。因此它将清除Gen 0,Gen 1和Gen 2,包括LOH。这称为完全垃圾收集,是最耗时的垃圾收集。对于许多应用程序来说,这是可以接受的。但绝对不是高性能Web服务器,在高性能Web服务器中,只需很少的大内存缓冲区即可处理一般的Web请求(从套接字读取,解压缩,解码JSON等)。
天真地将大型请求或响应主体存储为单个,byte[]
或者string
可能会导致LOH中的空间快速用完,并且由于正在运行完整的GC,可能会导致应用程序出现性能问题。
使用缓冲和同步读写来替代异步读写
当使用仅支持同步读写的序列化器/反序列化器时(例如JSON.NET),则在将数据传递到序列化器/反序列化器之前,最好将其缓冲到内存中。
💡注意:如果请求量很大,可能会导致内存不足问题,从而导致拒绝服务。请参阅此以获取更多信息。
不要在字段中存储IHttpContextAccessor.HttpContext
从请求线程进行访问时,IHttpContextAccessor.HttpContext
会返回HttpContext
活动请求的。不应将其存储在字段或变量中。
❌ 错误实践:此示例将HttpContext存储在一个字段中,然后尝试稍后使用它。
public class MyType { private readonly HttpContext _context; public MyType(IHttpContextAccessor accessor) { _context = accessor.HttpContext; } public void CheckAdmin() { if (!_context.User.IsInRole("admin")) { throw new UnauthorizedAccessException("当前用户不是https://jhrs.com 江湖人士管理员。"); } } }
asp.net core异步编程最佳实践,上面的逻辑很可能会在构造函数中捕获一个null或虚假的HttpContext,以供以后使用。
✅ 最佳实践:此示例将IHttpContextAccesor本身存储在一个字段中,并在正确的时间使用HttpContext字段(检查是否为空)。
public class MyType { private readonly IHttpContextAccessor _accessor; public MyType(IHttpContextAccessor accessor) { _accessor = accessor; } public void CheckAdmin() { var context = _accessor.HttpContext; if (context != null && !context.User.IsInRole("admin")) { throw new UnauthorizedAccessException("当前用户不是https://jhrs.com 网站管理员。"); } } }
不要从多个线程并行访问HttpContext。 它不是线程安全的
asp.net core异步编程最佳实践,HttpContext不是线程安全的。 从多个线程并行访问它可能导致损坏,从而导致未定义的行为(挂起,崩溃,数据损坏)。
❌错误实践:此示例发出3个并行请求,并在传出的HTTP请求之前和之后记录传入的请求路径。 这可能会并行地从多个线程访问请求路径。
public class AsyncController : Controller { [HttpGet("/search")] public async Task<SearchResults> Get(string query) { var query1 = SearchAsync(SearchEngine.Google, query); var query2 = SearchAsync(SearchEngine.Bing, query); var query3 = SearchAsync(SearchEngine.DuckDuckGo, query); await Task.WhenAll(query1, query2, query3); var results1 = await query1; var results2 = await query2; var results3 = await query3; return SearchResults.Combine(results1, results2, results3); } private async Task<SearchResults> SearchAsync(SearchEngine engine, string query) { var searchResults = SearchResults.Empty; try { _logger.LogInformation("开始从https://jhrs.com搜索 {path}.", HttpContext.Request.Path); searchResults = await _searchService.SearchAsync(engine, query); _logger.LogInformation("已完成从https://jhrs.com搜索 {path}.", HttpContext.Request.Path); } catch (Exception ex) { _logger.LogError(ex, "从jhrs.com搜索失败。 {path}", HttpContext.Request.Path); } return searchResults; } }
✅ 最佳实践:此示例在发出3个并行请求之前复制了传入请求中的所有数据。
public class AsyncController : Controller { [HttpGet("/search")] public async Task<SearchResults> Get(string query) { string path = HttpContext.Request.Path; var query1 = SearchAsync(SearchEngine.Google, query, path); var query2 = SearchAsync(SearchEngine.Bing, query, path); var query3 = SearchAsync(SearchEngine.DuckDuckGo, query, path); await Task.WhenAll(query1, query2, query3); var results1 = await query1; var results2 = await query2; var results3 = await query3; return SearchResults.Combine(results1, results2, results3); } private async Task<SearchResults> SearchAsync(SearchEngine engine, string query, string path) { var searchResults = SearchResults.Empty; try { _logger.LogInformation("开始从https://jhrs.com搜索 {path}.", path); searchResults = await _searchService.SearchAsync(engine, query); _logger.LogInformation("已完成从https://jhrs.com搜索 {path}.", path); } catch (Exception ex) { _logger.LogError(ex, "从jhrs.com搜索失败 {path}", path); } return searchResults; } }
请求完成后不要使用HttpContext
asp.net core异步编程最佳实践
,HttpContext
仅在运行中有活动的http请求时,此命令才有效。整个ASP.NET Core管道是执行每个请求的委托的异步链。当Task
从这个链条完成返回时,HttpContext
被回收。
❌ 错误实践:此示例使用异步void(这在ASP.NET Core应用程序中总是很糟糕),因此,HttpResponse
在http请求完成后访问。结果将导致进程崩溃。
public class AsyncVoidController : Controller { [HttpGet("/async")] public async void Get() { await Task.Delay(1000); // THIS will crash the process since we're writing after the response has completed on a background thread await Response.WriteAsync("你好,江湖人士。 https://jhrs.com"); } }
✅ 最佳实践:此示例将一个Task返回到框架,以便在整个操作完成之前http请求不会完成。
public class AsyncController : Controller { [HttpGet("/async")] public async Task Get() { await Task.Delay(1000); await Response.WriteAsync("你好,江湖人士。 https://jhrs.com"); } }
不要在后台线程中捕获HttpContext
❌ 错误实践:此示例显示闭包正在从Controller属性捕获HttpContext。 这很糟糕,因为该工作项可能在请求范围之外运行,结果可能导致读取伪造的HttpContext。
[HttpGet("/fire-and-forget-1")] public IActionResult FireAndForget1() { _ = Task.Run(() => { await Task.Delay(1000); // This closure is capturing the context from the Controller property. This is bad because this work item could run // outside of the http request leading to reading of bogus data. var path = HttpContext.Request.Path; Log(path); }); return Accepted(); }
✅ 最佳实践:此示例在请求期间显式复制了后台任务中所需的数据,并且未引用控制器本身的任何内容。
[HttpGet("/fire-and-forget-3")] public IActionResult FireAndForget3() { string path = HttpContext.Request.Path; _ = Task.Run(async () => { await Task.Delay(1000); // This captures just the path Log(path); }); return Accepted(); }
不要捕获在后台线程中注入控制器的服务
❌错误实践:此示例显示闭包正在从Controller动作参数捕获DbContext。 这很不好,因为此工作项可能在请求范围之外运行,而PokemonDbContext的范围仅限于请求。 结果,这将导致ObjectDisposedException。
[HttpGet("/fire-and-forget-1")] public IActionResult FireAndForget1([FromServices]PokemonDbContext context) { _ = Task.Run(() => { await Task.Delay(1000); // This closure is capturing the context from the Controller action parameter. This is bad because this work item could run // outside of the request scope and the PokemonDbContext is scoped to the request. As a result, this throw an ObjectDisposedException context.Pokemon.Add(new Pokemon()); await context.SaveChangesAsync(); }); return Accepted(); }
✅ 最佳实践:此示例将注入IServiceScopeFactory并在后台线程中创建新的依赖项注入作用域,并且不会引用控制器本身的任何内容。
[HttpGet("/fire-and-forget-3")] public IActionResult FireAndForget3([FromServices]IServiceScopeFactory serviceScopeFactory) { // This version of fire and forget adds some exception handling. We're also no longer capturing the PokemonDbContext from the incoming request. // Instead, we're injecting an IServiceScopeFactory (which is a singleton) in order to create a scope in the background work item. _ = Task.Run(async () => { await Task.Delay(1000); // Create a scope for the lifetime of the background operation and resolve services from it using (var scope = serviceScopeFactory.CreateScope()) { // This will a PokemonDbContext from the correct scope and the operation will succeed var context = scope.ServiceProvider.GetRequiredService<PokemonDbContext>(); context.Pokemon.Add(new Pokemon()); await context.SaveChangesAsync(); } }); return Accepted(); }
避免在HttpResponse启动后添加请求头
asp.net core异步编程最佳实践,ASP.NET Core不会缓冲http响应正文。 这意味着,第一次写入响应时,标头与主体的那一部分一起发送到客户端。 发生这种情况时,将无法再更改响应头。
❌ 错误实践:此逻辑尝试在响应已经开始之后添加响应头。
app.Use(async (next, context) => { await context.Response.WriteAsync("Hello "); await next(); // This may fail if next() already wrote to the response context.Response.Headers["test"] = "你好,江湖人士。 https://jhrs.com"; });
✅ 最佳实践:此示例在写入正文之前检查http响应是否已开始。
app.Use(async (next, context) => { await context.Response.WriteAsync("Hello "); await next(); // Check if the response has already started before adding header and writing if (!context.Response.HasStarted) { context.Response.Headers["test"] = "你好,江湖人士。 https://jhrs.com"; } });
✅ 最佳实践:此示例使用HttpResponse.OnStarting设置标头,然后将响应标头刷新到客户端。.
asp.net core异步编程最佳实践,它允许您注册将在将响应头写入客户端之前调用的回调。 它使您能够及时添加或覆盖标头,而无需了解管道中的下一个中间件。
app.Use(async (next, context) => { // Wire up the callback that will fire just before the response headers are sent to the client. context.Response.OnStarting(() => { context.Response.Headers["someheader"] = "你好,江湖人士。 https://jhrs.com"; return Task.CompletedTask; }); await next(); });
asp.net core异步编程最佳实践结论
.net 5今年已经发布,很多人都在拥抱.net 5,并使用asp.net core来开发新的项目,尽管.net core灭霸现在国内势头远不如其它编程语言,但整体是在向好;而对于.net 开发人员来说,了解一些asp.net core异步编程最佳实践并无坏处。
【江湖人士】(jhrs.com) 投稿作者:IT菜鸟,不代表江湖人士立场,如若转载,请注明出处:https://jhrs.com/2020/39170.html
扫码加入电报群,让你获得国外网赚一手信息。
文章标题:asp.net core异步编程最佳实践