asp.net core异步编程最佳实践

asp.net core异步编程最佳实践,ASP.NET Core是一个跨平台的高性能开放源代码框架,用于构建现代的,基于云的,Internet连接的应用程序。本指南捕获了编写可伸缩服务器应用程序时的一些常见陷阱和实践。

asp.net core异步编程最佳实践,ASP.NET Core是一个跨平台的高性能开放源代码框架,用于构建现代的,基于云的,Internet连接的应用程序。本指南捕获了编写可伸缩服务器应用程序时的一些常见陷阱和实践。

asp.net core异步编程最佳实践

继上一篇C#异步编程最佳实践发布之后,本文来讨论一下ASP.NET Core异步编程实践的一些实用指南,本文来自于github的英文翻译,喜欢阅读原文的可以点击这里

asp.net core异步编程最佳实践
C#异步编程最佳实践

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

User Review
0 (0 votes)
本站最新优惠

Namesilo优惠:新用户省 $1 域名注册-优惠码:45D%UYTcxYuCloZ 国外最便宜域名!点击了解更多

特别优惠:免费赠送 $100 Vultr主机-限时优惠!英文站必备海外服务器!点击了解更多

VPS优惠:搬瓦工优惠码:BWH3OGRI2BMW 最高省5.83%打开外面世界的一款主机点击了解更多

本文来自投稿作者:IT菜鸟,不代表江湖人士立场,如若转载,请注明出处:https://jhrs.com/2020/39170.html

发表评论

登录后才能评论