ASP.NET Core Guidance
原文:https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/tree/master
ASP.NET Core is a cross-platform, high-performance, open-source framework for building modern, cloud-based, Internet-connected applications. This guide captures some of the common pitfalls and practices when writing scalable server applications.
ASP.NET Core 是一个跨平台、高性能的开源框架,用于构建基于云且连接 Internet 的现代应用程序。本指南总结了编写可扩展服务器应用程序时的一些常见陷阱和实践。
Avoid using synchronous Read/Write overloads on HttpRequest.Body and HttpResponse.Body
避免对 HttpRequest.Body 和 HttpResponse.Body 使用同步 Read/Write 重载
All IO in ASP.NET Core is asynchronous. Servers implement the Stream interface which has both synchronous and asynchronous overloads. The asynchronous ones should be preferred to avoid blocking thread pool threads (this could lead to thread pool starvation).
ASP.NET Core 中的所有 IO 都是异步的。服务器实现了同时具有同步和异步重载的 Stream 接口。应优先使用异步重载,以免阻塞线程池线程(这可能导致线程池饥饿)。
❌ BAD This example uses the StreamReader.ReadToEnd and as a result blocks the current thread to wait for the result. This is an example of sync over async.
❌ 错误做法 此示例使用 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);
}
}
✅ GOOD This example uses StreamReader.ReadToEndAsync and as a result, does not block the thread while reading.
✅ 正确做法 此示例使用 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);
}
}
💡NOTE: If the request is large it could lead to out of memory problems which can result in a Denial Of Service. See this for more information.
💡注意:如果请求很大,可能会导致内存不足问题,进而造成拒绝服务。有关更多信息,请参阅此处。
Prefer using HttpRequest.ReadFormAsync() over HttpRequest.Form
优先使用 HttpRequest.ReadFormAsync(),而不是 HttpRequest.Form
You should always prefer HttpRequest.ReadFormAsync() over HttpRequest.Form. The only time it is safe to use HttpRequest.Form is the form has already been read by a call to HttpRequest.ReadFormAsync() and the cached form value is being read using HttpRequest.Form.
应始终优先使用 HttpRequest.ReadFormAsync(),而不是 HttpRequest.Form。只有在已调用 HttpRequest.ReadFormAsync() 读取表单,并使用 HttpRequest.Form 读取缓存的表单值时,使用 HttpRequest.Form 才是安全的。
❌ BAD This example uses HttpRequest.Form uses sync over async under the covers and can lead to thread pool starvation (in some cases).
❌ 错误做法 此示例使用 HttpRequest.Form,其内部采用同步等待异步,在某些情况下可能导致线程池饥饿。
public class MyController : Controller
{
[HttpPost("/form-body")]
public IActionResult Post()
{
var form = HttpRequest.Form;
Process(form["id"], form["name"]);
return Accepted();
}
}
✅ GOOD This example uses HttpRequest.ReadFormAsync() to read the form body asynchronously.
✅ 正确做法 此示例使用 HttpRequest.ReadFormAsync() 异步读取表单正文。
public class MyController : Controller
{
[HttpPost("/form-body")]
public async Task<IActionResult> Post()
{
var form = await HttpRequest.ReadFormAsync();
Process(form["id"], form["name"]);
return Accepted();
}
}
Avoid reading large request bodies or response bodies into memory
避免将大型请求正文或响应正文读入内存
In .NET any single object allocation greater than 85KB ends up in the large object heap (LOH). Large objects are expensive in 2 ways:
在 .NET 中,任何大于 85KB 的单个对象分配都会进入大对象堆(LOH)。大对象会在两个方面带来高昂开销:
- The allocation cost is high because the memory for a newly allocated large object has to be cleared (the CLR guarantees that memory for all newly allocated objects is cleared)
分配成本很高,因为必须清零新分配给大对象的内存(CLR 保证清零所有新分配对象的内存) - LOH is collected with the rest of the heap (it requires a "full garbage collection" or Gen2 collection)
LOH 与堆的其余部分一起回收(需要“完整垃圾回收”或 Gen2 回收)
This blog post describes the problem succinctly:
这篇博客文章简洁地描述了这个问题:
When a large object is allocated, it’s marked as Gen 2 object. Not Gen 0 as for small objects. The consequences are that if you run out of memory in LOH, GC cleans up whole managed heap, not only LOH. So it cleans up Gen 0, Gen 1 and Gen 2 including LOH. This is called full garbage collection and is the most time-consuming garbage collection. For many applications, it can be acceptable. But definitely not for high-performance web servers, where few big memory buffers are needed to handle an average web request (read from a socket, decompress, decode JSON & more).
分配大对象时,该对象会被标记为 Gen 2 对象,而不是像小对象那样标记为 Gen 0。其后果是,如果 LOH 内存耗尽,GC 将清理整个托管堆,而不仅是 LOH。因此,它会清理 Gen 0、Gen 1 和 Gen 2(包括 LOH)。这称为完整垃圾回收,也是最耗时的垃圾回收。对许多应用程序来说,这可能可以接受;但对于高性能 Web 服务器绝对不可接受,因为处理一个普通 Web 请求就需要若干大型内存缓冲区(从套接字读取、解压缩、解码 JSON 等)。
Naively storing a large request or response body into a single byte[] or string may result in quickly running out of space in the LOH and may cause performance issues for your application because of full GCs running.
直接将大型请求正文或响应正文存储到单个 byte[] 或 string 中,可能会使 LOH 空间迅速耗尽,并可能因运行完整 GC 而导致应用程序出现性能问题。
Use buffered and synchronous reads and writes as an alternative to asynchronous reading and writing
使用缓冲和同步读写替代异步读写
When using a serializer/de-serializer that only supports synchronous reads and writes (like JSON.NET) then prefer buffering the data into memory before passing data into the serializer/de-serializer.
使用仅支持同步读写的序列化器/反序列化器(如 JSON.NET)时,应优先将数据缓冲到内存中,再将数据传递给序列化器/反序列化器。
💡NOTE: If the request is large it could lead to out of memory problems which can result in a Denial Of Service. See this for more information.
💡注意:如果请求很大,可能会导致内存不足问题,进而造成拒绝服务。有关更多信息,请参阅此处。
Do not store IHttpContextAccessor.HttpContext in a field
不要将 IHttpContextAccessor.HttpContext 存储在字段中
The IHttpContextAccessor.HttpContext will return the HttpContext of the active request when accessed from the request thread. It should not be stored in a field or variable.
从请求线程访问 IHttpContextAccessor.HttpContext 时,它会返回活动请求的 HttpContext。不应将其存储在字段或变量中。
❌ BAD This example stores the HttpContext in a field then attempts to use it later.
❌ 错误做法 此示例将 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("The current user isn't an admin");
}
}
}
The above logic will likely capture a null or bogus HttpContext in the constructor for later use.
上述逻辑很可能在构造函数中捕获一个为 null 或无效的 HttpContext,供以后使用。
✅ GOOD This example stores the IHttpContextAccessor itself in a field and uses the HttpContext field at the correct time (checking for null).
✅ 正确做法 此示例将 IHttpContextAccessor 本身存储在字段中,并在正确的时间使用 HttpContext 字段(同时检查 null)。
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("The current user isn't an admin");
}
}
}
Do not access the HttpContext from multiple threads in parallel. It is not thread safe.
不要从多个线程并行访问 HttpContext,它不是线程安全的
The HttpContext is NOT threadsafe. Accessing it from multiple threads in parallel can cause corruption resulting in undefined behavior (hangs, crashes, data corruption).
HttpContext 不是线程安全的。从多个线程并行访问它可能造成损坏,从而导致未定义行为(挂起、崩溃或数据损坏)。
❌ BAD This example makes 3 parallel requests and logs the incoming request path before and after the outgoing http request. This accesses the request path from multiple threads potentially in parallel.
❌ 错误做法 此示例发出 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("Starting search query from {path}.", HttpContext.Request.Path);
searchResults = await _searchService.SearchAsync(engine, query);
_logger.LogInformation("Finishing search query from {path}.", HttpContext.Request.Path);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed query from {path}", HttpContext.Request.Path);
}
return searchResults;
}
}
✅ GOOD This example copies all data from the incoming request before making the 3 parallel requests.
✅ 正确做法 此示例在发出 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("Starting search query from {path}.", path);
searchResults = await _searchService.SearchAsync(engine, query);
_logger.LogInformation("Finishing search query from {path}.", path);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed query from {path}", path);
}
return searchResults;
}
}
Do not use the HttpContext after the request is complete
请求完成后不要使用 HttpContext
The HttpContext is only valid as long as there is an active http request in flight. The entire ASP.NET Core pipeline is an asynchronous chain of delegates that executes every request. When the Task returned from this chain completes, the HttpContext is recycled.
HttpContext 仅在有活动 HTTP 请求正在处理时有效。整个 ASP.NET Core 管道是一个执行每个请求的异步委托链。当此委托链返回的 Task 完成时,HttpContext 会被回收。
❌ BAD This example uses async void (which is a ALWAYS bad in ASP.NET Core applications) and as a result, accesses the HttpResponse after the http request is complete. It will crash the process as a result.
❌ 错误做法 此示例使用 async void(在 ASP.NET Core 应用程序中,这种做法始终是错误的),因此会在 HTTP 请求完成后访问 HttpResponse,最终导致进程崩溃。
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("Hello World");
}
}
✅ GOOD This example returns a Task to the framework so the http request doesn't complete until the entire action completes.
✅ 正确做法 此示例向框架返回 Task,使 HTTP 请求在整个操作完成之前不会完成。
public class AsyncController : Controller
{
[HttpGet("/async")]
public async Task Get()
{
await Task.Delay(1000);
await Response.WriteAsync("Hello World");
}
}
Do not capture the HttpContext in background threads
不要在后台线程中捕获 HttpContext
❌ BAD This example shows a closure is capturing the HttpContext from the Controller property. This is bad because this work item could run
outside of the request scope and as a result, could lead to reading a bogus 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();
}
✅ GOOD This example copies the data required in the background task during the request explicitly and does not reference
anything from the controller itself.
✅ 正确做法 此示例在请求期间显式复制后台任务所需的数据,并且不引用
控制器本身的任何内容。
[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();
}
Do not capture services injected into the controllers on background threads
不要在后台线程中捕获注入控制器的服务
❌ BAD This example shows a closure is capturing the DbContext 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 will end up with an ObjectDisposedException.
❌ 错误做法 此示例展示了一个闭包从 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 throws an ObjectDisposedException
context.Pokemon.Add(new Pokemon());
await context.SaveChangesAsync();
});
return Accepted();
}
✅ GOOD This example injects an IServiceScopeFactory and creates a new dependency injection scope in the background thread and does not reference
anything from the controller itself.
✅ 正确做法 此示例注入 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 resolve 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();
}
Avoid adding headers after the HttpResponse has started
避免在 HttpResponse 开始后添加标头
ASP.NET Core does not buffer the http response body. This means that the very first time the response is written, the headers are sent along with that chunk of the body to the client. When this happens, it's no longer possible to change response headers.
ASP.NET Core 不会缓冲 HTTP 响应正文。这意味着首次写入响应时,标头会随该正文块一起发送到客户端。发生这种情况后,便无法再更改响应标头。
❌ BAD This logic tries to add response headers after the response has already started.
❌ 错误做法 此逻辑尝试在响应已经开始后添加响应标头。
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"] = "value";
});
✅ GOOD This example checks if the http response has started before writing to the body.
✅ 正确做法 此示例在写入正文前检查 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"] = "value";
}
});
✅ GOOD This example uses HttpResponse.OnStarting to set the headers before the response headers are flushed to the client.
✅ 正确做法 此示例使用 HttpResponse.OnStarting,在响应标头刷新到客户端之前设置标头。
It allows you to register a callback that will be invoked just before response headers are written to the client. It gives you the ability to append or override headers just in time, without requiring knowledge of the next middleware in the pipeline.
它允许注册一个回调,该回调会在响应标头即将写入客户端之前调用。这样可以及时追加或替换标头,而不需要了解管道中的下一个中间件。
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"] = "somevalue";
return Task.CompletedTask;
});
await next();
});

浙公网安备 33010602011771号