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异步编程最佳实践并无坏处。

