相关文章推荐
暴躁的人字拖  ·  EntityFramework ...·  1 年前    · 

本文提供了有关最大程度地提高 ASP.NET Core 应用的性能和可靠性的准则。

充分利用缓存

缓存在本文的多个部分中进行了讨论。 有关详细信息,请参阅 ASP.NET Core 中的缓存概述

了解热代码路径

在本文中,热代码路径定义为经常调用并形成大量执行时间的代码路径。 热代码路径通常会限制应用横向扩展和性能,在本文的多个部分中进行了讨论。

避免阻塞调用

ASP.NET Core 应用应设计为可同时处理许多请求。 异步 API 允许较小线程池处理数千个并发请求,无需等待阻塞调用。 线程可以处理另一个请求,而不是等待长时间运行的同步任务完成。

ASP.NET Core 应用中的一个常见性能问题是阻塞可以异步进行的调用。 许多同步阻塞调用都会导致 线程池饥饿 和响应时间降低。

请勿通过调用 Task.Wait Task<TResult>.Result 来阻止异步执行。 请勿获取常见代码路径中的锁。 当构建为并行运行代码时,ASP.NET Core 应用的性能最佳。 请勿调用 Task.Run 并立即等待它。 ASP.NET Core 已经在普通线程池线程上运行应用代码,因此调用 Task.Run 只会导致不必要的额外线程池计划。 即使计划的代码会阻止某个线程, Task.Run 也不会阻止该线程。

  • 务必使 热代码路径 异步。
  • 如果有异步 API 可用,则务必异步调用数据访问、I/O 和长时间运行的操作 API。
  • 请勿使用 Task.Run 使同步 API 异步。
  • 务必使控制器/Razor Page 操作异步。 为了获益于 async/await 模式,整个调用堆栈都是异步的。
  • 探查器(例如 PerfView )可用于查找频繁添加到 线程池 中的线程。 Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start 事件指示添加到线程池的线程。

    跨多个较小页面返回大型集合

    网页不应一次加载大量数据。 返回对象集合时,请考虑它是否会导致性能问题。 确定设计是否可能会产生以下不良结果:

  • OutOfMemoryException 或占用大量内存
  • 线程池资源不足(请参阅以下有关 IAsyncEnumerable<T> 的注解)
  • 响应时间缓慢
  • 频繁的垃圾回收
  • 请添加分页以缓解以上情形。 使用页面大小和页面索引参数时,开发人员应支持返回部分结果的设计。 当需要详尽结果时,应使用分页来异步填充结果批次,以避免锁定服务器资源。

    有关分页和限制返回的记录数的详细信息,请参阅:

  • 性能注意事项
  • 将分页添加到 ASP.NET Core 应用
  • 返回 IEnumerable<T> IAsyncEnumerable<T>

    从操作返回 IEnumerable<T> 会导致序列化程序同步集合迭代。 因此会阻止调用,并且可能会导致线程池资源不足。 若要避免同步枚举,请在返回可枚举内容前使用 ToListAsync

    从 ASP.NET Core 3.0 开始, IAsyncEnumerable<T> 可用作异步枚举的 IEnumerable<T> 的替代方法。 有关详细信息,请参阅 控制器操作返回类型

    最大程度减少大型对象分配

    .NET Core 垃圾回收器 在 ASP.NET Core 应用中自动管理内存分配和释放。 自动垃圾回收通常意味着开发人员无需担心如何或何时释放内存。 但是,清理未引用的对象会占用 CPU 时间,因此开发人员应最大限度减少 热代码路径 中的对象分配。 垃圾回收在大型对象(> = 85,000 字节)上成本特别高昂。 大型对象存储在 大型对象堆 上,需要完整(第 2 代)垃圾回收才能清理。 与第 0 代和第 1 代回收不同,第 2 代回收需要临时暂停应用执行。 频繁分配和取消分配大型对象可能会导致性能不一致。

  • 请考虑缓存经常使用的大型对象。 缓存大型对象会阻止进行成本高昂的分配。
  • 务必使用 ArrayPool<T> 形成池缓冲区以存储大型数组。
  • 请勿在 热代码路径 上分配许多生存期较短的大型对象。
  • 可以通过在 PerfView 中查看垃圾回收 (GC) 统计信息并检查以下内容来诊断内存问题(如前面的问题):

  • 垃圾回收暂停时间。
  • 花费在垃圾回收上的处理器时间百分比。
  • 第 0 代、第 1 代和第 2 代的垃圾回收量。
  • 有关详细信息,请参阅 垃圾回收和性能

    优化数据访问和 I/O

    与数据存储和其他远程服务的交互通常是 ASP.NET Core 应用的最慢部分。 高效读取和写入数据对于良好的性能至关重要。

  • 请异步调用所有数据访问 API。
  • 请勿检索不需要的数据。 编写查询以便仅返回当前 HTTP 请求所需的数据。
  • 如果可接受稍微过时的数据,请考虑缓存从数据库或远程服务检索的经常访问的数据。 根据方案使用 MemoryCache DistributedCache 。 有关详细信息,请参阅 ASP.NET Core 中的响应缓存
  • 请尽量缩短网络往返。 目标是在单个调用而不是多个调用中检索所需数据。
  • 当出于只读目的访问数据时, 在Entity Framework Core中使用 无跟踪查询 。 EF Core 可以更有效地返回无跟踪查询的结果。
  • 请筛选和聚合 LINQ 查询(例如使用 .Where .Select .Sum 语句),以便数据库执行筛选。
  • 务必考虑到 EF Core 会在客户端上解析一些查询运算符,这可能会导致查询执行效率低下。 有关详细信息,请参阅 客户端评估性能问题
  • 请勿对集合使用投影查询,这可能会导致执行“N + 1”个 SQL 查询。 有关详细信息,请参阅 相关子查询优化
  • 以下方法可以提高大规模应用的性能:

  • DbContext 池
  • 显式编译的查询
  • 建议在提交基本代码之前衡量前面高性能方法的影响。 已编译查询的额外复杂性可能无法证明性能改进的合理性。

    通过使用 Application Insights 或分析工具查看访问数据所用的时间,可以检测到查询问题。 大多数数据库还提供有关频繁执行的查询的统计信息。

    与 HttpClientFactory 之间的池 HTTP 连接

    尽管 HttpClient 实现了 IDisposable 接口,不过它设计为可重用。 关闭的 HttpClient 实例使套接字在短时间内以 TIME_WAIT 状态保持打开。 如果经常使用创建和释放 HttpClient 对象的代码路径,则应用可能会耗尽可用的套接字。 在 ASP.NET Core 2.1 中引入了 HttpClientFactory ,以作为此问题的解决方案。 它会处理池 HTTP 连接以优化性能和可靠性。 有关详细信息,请参阅 使用 HttpClientFactory 实现可复原的 HTTP 请求

  • 请勿直接创建和释放 HttpClient 实例。
  • 请勿使用 HttpClientFactory 检索 HttpClient 实例。 有关详细信息,请参阅 使用 HttpClientFactory 实现可复原的 HTTP 请求
  • 使常用代码路径保持快速

    你希望所有代码都可快速执行。 经常调用的代码路径是优化的关键。 其中包括:

  • 应用请求处理管道中的中间件组件,尤其是在管道中早期运行的中间件。 这些组件对性能具有很大影响。
  • 对每个请求都执行或是按请求执行多次的代码。 例如,自定义日志记录、授权处理程序或暂时性服务的初始化。
  • 请勿将自定义中间件组件用于长时间运行的任务。
  • 请使用性能分析工具(例如 Visual Studio 诊断工具 PerfView )标识 热代码路径
  • 在 HTTP 请求外部完成长时间运行的任务

    对 ASP.NET Core 应用进行的大多数请求可以由调用必要服务并返回 HTTP 响应的控制器或页面模型进行处理。 对于涉及长时间运行的任务的一些请求,最好使整个请求-响应过程异步进行。

  • 在普通 HTTP 请求处理过程中,请勿等待长时间运行的任务完成。
  • 请考虑使用 后台服务 处理长时间运行的请求,或使用 Azure 函数 进行进程外处理。 在进程外完成工作对于 CPU 密集型任务尤其有利。
  • 请使用实时通信选项(如 SignalR )以异步方式与客户端通信。
  • 缩小客户端资产

    具有复杂前端的 ASP.NET Core 应用会经常处理许多 JavaScript、CSS 或图像文件。 初始加载请求的性能可以通过以下方式得到提高:

  • 捆绑,即将多个文件合并为一个文件。
  • 缩小,即通过删除空格和注释来减小文件的大小。
  • 务必使用 捆绑和缩小准则 ,其中提及了兼容工具,并演示如何使用 ASP.NET Core 的 environment 标记处理 Development Production 环境。
  • 请考虑使用其他第三方工具(如 Webpack )进行复杂客户端资产管理。
  • 减小响应大小通常可显著提高应用的响应速度。 减小有效负载大小的一种方式是压缩应用的响应。 有关详细信息,请参阅 响应压缩

    使用最新 ASP.NET Core 版本

    每个新版本的 ASP.NET Core 都包含性能改进。 .NET Core 和 ASP.NET Core 中的优化意味着较新版本的性能通常优于较旧版本。 例如,.NET Core 2.1 添加了对已编译正则表达式的支持,可受益于 Span<T> 。 ASP.NET Core 2.2 添加了对 HTTP/2 的支持。 ASP.NET Core 3.0 添加了许多改进 ,可减少内存使用量并提高吞吐量。 如果性能是优先事项,请考虑升级到当前版本的 ASP.NET Core。

    尽量减少异常

    异常应很少出现。 相对于其他代码流模式,引发和捕获异常的速度较慢。 因此,不应使用异常来控制正常程序流。

  • 请勿将引发或捕获异常用作正常程序流的一种方法(尤其是在 热代码路径 中)。
  • 请在应用中包含逻辑,以检测和处理会导致异常的状况。
  • 对于不寻常或意外状况,请引发或捕获异常。
  • 应用诊断工具(如 Application Insights)可帮助识别应用中可能会影响性能的常见异常。

    避免对 HttpRequest/HttpResponse 正文进行同步读取或写入

    ASP.NET Core 中的所有 I/O 都是异步的。 服务器会实现 Stream 接口,该接口同时具有同步和异步重载。 应首选异步重载以避免阻止线程池线程。 阻止线程可能会导致线程池资源不足。

    请勿这样做:下面的示例使用 ReadToEnd 。 它会阻止当前线程等待结果。 下面是 异步中同步 的示例。

    public class BadStreamReaderController : Controller
        [HttpGet("/contoso")]
        public ActionResult<ContosoData> Get()
            var json = new StreamReader(Request.Body).ReadToEnd();
            return JsonSerializer.Deserialize<ContosoData>(json);
    

    在上面的代码中,Get 将整个 HTTP 请求正文同步读取到内存中。 如果客户端上传速度缓慢,则应用会进行异步中同步。 应用进行异步中同步是因为 Kestrel 不支持同步读取。

    请这样做:下面的示例使用 ReadToEndAsync,在读取时不会阻止线程。

    public class GoodStreamReaderController : Controller
        [HttpGet("/contoso")]
        public async Task<ActionResult<ContosoData>> Get()
            var json = await new StreamReader(Request.Body).ReadToEndAsync();
            return JsonSerializer.Deserialize<ContosoData>(json);
    

    上面的代码会将整个 HTTP 请求正文同步读取到内存中。

    如果请求较大,则将整个 HTTP 请求正文读取到内存中可能会导致内存不足 (OOM) 状况。 OOM 可能会导致拒绝服务。 有关详细信息,请参阅本文中的避免将大型请求正文或响应正文读取到内存中

    请这样做:下面的示例使用非缓冲请求正文完全异步进行:

    public class GoodStreamReaderController : Controller
        [HttpGet("/contoso")]
        public async Task<ActionResult<ContosoData>> Get()
            return await JsonSerializer.DeserializeAsync<ContosoData>(Request.Body);
    

    上面的代码将请求正文异步反序列化为 C# 对象。

    首选 ReadFormAsync 而不是 Request.Form

    请使用 HttpContext.Request.ReadFormAsync,而不是 HttpContext.Request.FormHttpContext.Request.Form 只有在以下状况下才能安全读取:

  • 通过调用 ReadFormAsync 读取了窗体,并且
  • 在使用 HttpContext.Request.Form 读取缓存的窗体值
  • 请勿这样做:下面的示例使用 HttpContext.Request.FormHttpContext.Request.Form 使用异步中同步,可能会导致线程池资源不足。

    public class BadReadController : Controller
        [HttpPost("/form-body")]
        public IActionResult Post()
            var form =  HttpContext.Request.Form;
            Process(form["id"], form["name"]);
            return Accepted();
    

    请这样做:下面的示例使用 HttpContext.Request.ReadFormAsync 异步读取窗体正文。

    public class GoodReadController : Controller
        [HttpPost("/form-body")]
        public async Task<IActionResult> Post()
           var form = await HttpContext.Request.ReadFormAsync();
            Process(form["id"], form["name"]);
            return Accepted();
    

    避免将大型请求正文或响应正文读取到内存中

    在 .NET 中,每个大于或等于 85,000 字节的对象分配最终都位于大型对象堆 (LOH) 中。 大型对象在两方面的成本十分高昂:

  • 分配成本较高,因为必须清除新分配的大型对象的内存。 CLR 可保证清除所有新分配的对象的内存。
  • LOH 与堆的其余部分一起收集。 LOH 需要完整垃圾回收第 2 代收集
  • 博客文章简洁地描述了问题:

    分配大型对象时,它标记为第 2 代对象。 而不是与小型对象一样标记为第 0 代。 后果是,如果在 LOH 中内存不足,GC 会清理整个托管堆,而不仅是 LOH。 因此它会清理第 0 代、第 1 代和第 2 代,包括 LOH。 这称为完整垃圾回收,是最耗时的垃圾回收。 对于许多应用程序,这可以接受。 但肯定不适用于高性能 Web 服务器,在这类服务器中,需要极少的大内存缓冲区来处理普通 Web 请求(从套接字读取、解压缩和解码 JSON 等)。

    将大型请求或响应正文存储到单个 byte[]string 中:

  • 可能会导致 LOH 中的空间快速用尽。
  • 由于运行完整 GC,可能会导致应用出现性能问题。
  • 使用同步数据处理 API

    使用仅支持同步读取和写入的序列化程序/反序列化程序(例如 Json.NET)时:

  • 先将数据异步缓冲到内存中,然后再将数据传递到序列化程序/反序列化程序。
  • 如果请求较大,则可能会导致内存不足 (OOM) 状况。 OOM 可能会导致拒绝服务。 有关详细信息,请参阅本文中的避免将大型请求正文或响应正文读取到内存中

    ASP.NET Core 3.0 默认使用 System.Text.Json 进行 JSON 序列化。 System.Text.Json

  • 以异步方式读取和写入 JSON。
  • 针对 UTF-8 文本进行了优化。
  • 通常比 Newtonsoft.Json 性能更高。
  • 请勿将 IHttpContextAccessor.HttpContext 存储在字段中

    从请求线程访问时,IHttpContextAccessor.HttpContext 会返回活动请求的 HttpContextIHttpContextAccessor.HttpContext 不应存储在字段或变量中。

    请勿这样做:下面的示例将 HttpContext 存储在字段中,然后尝试在以后使用它。

    public class MyBadType
        private readonly HttpContext _context;
        public MyBadType(IHttpContextAccessor accessor)
            _context = accessor.HttpContext;
        public void CheckAdmin()
            if (!_context.User.IsInRole("admin"))
                throw new UnauthorizedAccessException("The current user isn't an admin");
    

    上面的代码经常在构造函数中捕获 Null 或不正确的 HttpContext

    请这样做:下面的示例:

  • IHttpContextAccessor 存储在字段中。
  • 在正确的时间使用 HttpContext 字段并检查是否存在 null
  • public class MyGoodType
        private readonly IHttpContextAccessor _accessor;
        public MyGoodType(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");
    

    请勿从多个线程访问 HttpContext

    HttpContext 不是线程安全型。 并行从多个线程访问 HttpContext 可能会导致意外的行为,例如挂起、崩溃和数据损坏。

    请勿这样做:下面的示例进行三个并行请求,并记录传出 HTTP 请求之前和之后的传入请求路径。 请求路径从多个线程进行访问(可能是并行访问)。

    public class AsyncBadSearchController : 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 = _searchService.Empty();
                _logger.LogInformation("Starting search query from {path}.", 
                                        HttpContext.Request.Path);
                searchResults = _searchService.Search(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 await searchResults;
    

    请这样做:下面的示例在进行三个并行请求之前,从传入请求复制所有数据。

    public class AsyncGoodSearchController : 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 = _searchService.Empty();
                _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 await searchResults;
    

    请求完成之后,请勿使用 HttpContext

    HttpContext 仅在 ASP.NET Core 管道中存在活动 HTTP 请求时有效。 整个 ASP.NET Core 管道是执行每个请求的异步委托链。 从此链返回的 Task 完成后,会回收 HttpContext

    请勿这样做:下面的示例使用 async void,它使 HTTP 请求在达到第一个 await 时完成:

  • 使用 async void 在 ASP.NET Core 应用中始终是不良做法。
  • 在 HTTP 请求完成之后,示例代码访问 HttpResponse
  • 延迟访问导致进程崩溃。
  • public class AsyncBadVoidController : Controller
        [HttpGet("/async")]
        public async void Get()
            await Task.Delay(1000);
            // The following line will crash the process because of writing after the 
            // response has completed on a background thread. Notice async void Get()
            await Response.WriteAsync("Hello World");
    

    请这样做:下面的示例将 Task 返回到框架,因此在操作完成之前,HTTP 请求不会完成。

    public class AsyncGoodTaskController : Controller
        [HttpGet("/async")]
        public async Task Get()
            await Task.Delay(1000);
            await Response.WriteAsync("Hello World");
    

    请勿在后台线程中捕获 HttpContext

    请勿这样做:下面的示例演示闭包从 Controller 属性捕获 HttpContext。 这是一种不良做法,因为工作项可能:

  • 在请求范围之外运行。
  • 尝试读取错误的 HttpContext
  • [HttpGet("/fire-and-forget-1")]
    public IActionResult BadFireAndForget()
        _ = Task.Run(async () =>
            await Task.Delay(1000);
            var path = HttpContext.Request.Path;
            Log(path);
        return Accepted();
    

    请这样做:下面的示例:

  • 在请求期间复制后台任务中所需的数据。
  • 不从控制器引用任何内容。
  • [HttpGet("/fire-and-forget-3")]
    public IActionResult GoodFireAndForget()
        string path = HttpContext.Request.Path;
        _ = Task.Run(async () =>
            await Task.Delay(1000);
            Log(path);
        return Accepted();
    

    后台任务应作为托管服务实现。 有关详细信息,请参阅使用托管服务的后台任务

    请勿在后台线程上捕获注入到控制器中的服务

    请勿这样做:下面的示例演示闭包从 Controller 操作参数捕获 DbContext。 这是一种不良做法。 工作项可能在请求范围之外运行。 ContosoDbContext 的范围限定为请求,从而导致 ObjectDisposedException

    [HttpGet("/fire-and-forget-1")]
    public IActionResult FireAndForget1([FromServices]ContosoDbContext context)
        _ = Task.Run(async () =>
            await Task.Delay(1000);
            context.Contoso.Add(new Contoso());
            await context.SaveChangesAsync();
        return Accepted();
    

    请这样做:下面的示例:

  • 注入 IServiceScopeFactory 以便在后台工作项中创建范围。 IServiceScopeFactory 是单一实例。
  • 在后台线程中创建新的依赖项注入范围。
  • 不从控制器引用任何内容。
  • 不从传入请求捕获 ContosoDbContext
  • [HttpGet("/fire-and-forget-3")]
    public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                        serviceScopeFactory)
        _ = Task.Run(async () =>
            await Task.Delay(1000);
            await using (var scope = serviceScopeFactory.CreateAsyncScope())
                var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();
                context.Contoso.Add(new Contoso());
                await context.SaveChangesAsync();                                        
        return Accepted();
    

    以下突出显示的代码:

  • 在后台操作生存期内创建范围,并解析其中的服务。
  • 从正确的范围使用 ContosoDbContext
  • [HttpGet("/fire-and-forget-3")]
    public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                        serviceScopeFactory)
        _ = Task.Run(async () =>
            await Task.Delay(1000);
            await using (var scope = serviceScopeFactory.CreateAsyncScope())
                var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();
                context.Contoso.Add(new Contoso());
                await context.SaveChangesAsync();                                        
        return Accepted();
    

    在响应正文启动之后,请勿修改状态代码或标头

    ASP.NET Core 不会缓冲 HTTP 响应正文。 首次写入响应时:

  • 标头与正文的该区块一起发送到客户端。
  • 无法再更改响应标头。
  • 请勿这样做:下面的代码尝试在响应已启动之后添加响应标头:

    app.Use(async (context, next) =>
        await next();
        context.Response.Headers["test"] = "test value";
    

    在上面的代码中,如果将 next() 写入响应,则 context.Response.Headers["test"] = "test value"; 会引发异常。

    请这样做:下面的示例在修改标头之前检查 HTTP 响应是否已启动。

    app.Use(async (context, next) =>
        await next();
        if (!context.Response.HasStarted)
            context.Response.Headers["test"] = "test value";
    

    请这样做:下面的示例使用 HttpResponse.OnStarting 在将响应标头刷新到客户端之前设置标头。

    通过检查响应是否未启动,可以在写入响应标头之前注册将调用的回调。 检查响应是否未启动:

  • 提供实时追加或替代标头的能力。
  • 不需要了解管道中的下一个中间件。
  • app.Use(async (context, next) =>
        context.Response.OnStarting(() =>
            context.Response.Headers["someheader"] = "somevalue";
            return Task.CompletedTask;
        await next();
    

    如果已开始向响应正文写入,请勿调用 next()

    只有在组件可以处理和操作响应时,才应调用组件。

    将进程内托管与 IIS 结合使用

    使用进程内托管,ASP.NET Core 在与其 IIS 工作进程相同的进程中运行。 进程内托管可提供比进程外托管更高的性能,因为请求不会通过环回适配器进行代理。 环回适配器是一个网络接口,用于将传出的网络流量返回给同一计算机。 IIS 使用 Windows 进程激活服务 (WAS) 处理进程管理。

    在 ASP.NET Core 3.0 及更高版本中,项目默认为进程内托管模型。

    有关详细信息,请参阅 使用 IIS 在 Windows 上托管 ASP.NET Core

    请勿假设 HttpRequest.ContentLength 不为 null

    如果未收到 Content-Length 标头,则 HttpRequest.ContentLength 为 null。 如果为 null,则表示请求正文的长度未知;这并不意味着长度为零。 因为所有与 null 的比较(除了 ==)都返回 false,例如,当请求正文大小大于 1024 时,比较 Request.ContentLength > 1024 可能返回 false。 不知道这可能会导致应用中出现安全漏洞。 你可能会认为你在防范过大的请求,而你却没有。

    有关详细信息,请参阅此 StackOverflow 答案

    可靠的 Web 应用模式

    请观看适用于 .NET 的可靠 Web 应用模式YouTube 视频文章,了解如何创建新式、可靠、高性能、可测试、经济高效且可缩放的 ASP.NET Core 应用,无论是从头开始创建还是重构现有应用。

    即将发布:在整个 2024 年,我们将逐步淘汰作为内容反馈机制的“GitHub 问题”,并将其取代为新的反馈系统。 有关详细信息,请参阅:https://aka.ms/ContentUserFeedback

    提交和查看相关反馈