├── Gotchas.md
├── Scenarios
├── Scenarios.csproj
├── Program.cs
├── Controllers
│ ├── EmphemeralOperationController.cs
│ ├── HttpClientController.cs
│ ├── BigJsonOutputController.cs
│ ├── AsyncVoidController.cs
│ ├── CancelLegacyOperationController.cs
│ ├── CancelAsyncOperationController.cs
│ ├── AsyncFactoryController.cs
│ ├── BigJsonInputController.cs
│ ├── AsyncOperationController.cs
│ └── FireAndForgetController.cs
├── Model
│ └── PokemonData.cs
├── PokemonDbContext.cs
├── Services
│ ├── EphemeralOperation.cs
│ ├── RemoteConnection.cs
│ ├── PokemonService.cs
│ └── LegacyService.cs
├── wwwroot
│ └── index.html
├── Startup.cs
└── Infrastructure
│ └── TaskExtensions.cs
├── Guidance.md
├── .gitignore
├── README.md
├── .gitattributes
├── AspNetCoreDiagnosticScenarios.sln
├── AspNetCoreGuidance.md
└── AsyncGuidance.md
/Gotchas.md:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Scenarios/Scenarios.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.1
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Guidance.md:
--------------------------------------------------------------------------------
1 | # Common Pitfalls writing scalable services in ASP.NET Core
2 |
3 | This document serves as a guide for writing scalable services in ASP.NET Core. Some of the guidance is general purpose but will be explained through the lens of writing
4 | web services. The examples shown here are based on experiences with customer applications and issues found on Github and Stack Overflow.
5 |
6 | - [General ASP.NET Core](AspNetCoreGuidance.md)
7 | - [Asynchronous Programming](AsyncGuidance.md)
8 | - [.NET API Gotchas](Gotchas.md)
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | [Oo]bj/
2 | [Bb]in/
3 | TestResults/
4 | .nuget/
5 | .build/
6 | .testPublish/
7 | publish/
8 | *.sln.ide/
9 | _ReSharper.*/
10 | packages/
11 | shared/
12 | artifacts/
13 | PublishProfiles/
14 | .vs/
15 | bower_components/
16 | node_modules/
17 | debugSettings.json
18 | project.lock.json
19 | *.user
20 | *.suo
21 | *.cache
22 | *.docstates
23 | _ReSharper.*
24 | nuget.exe
25 | *net45.csproj
26 | *net451.csproj
27 | *k10.csproj
28 | *.psess
29 | *.vsp
30 | *.pidb
31 | *.userprefs
32 | *DS_Store
33 | *.ncrunchsolution
34 | *.*sdf
35 | *.ipch
36 | .settings
37 | *.sln.ide
38 | node_modules
39 | *launchSettings.json
40 | *.orig
41 |
--------------------------------------------------------------------------------
/Scenarios/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.Extensions.Configuration;
9 | using Microsoft.Extensions.Logging;
10 |
11 | namespace Scenarios
12 | {
13 | public class Program
14 | {
15 | public static void Main(string[] args)
16 | {
17 | CreateWebHostBuilder(args).Build().Run();
18 | }
19 |
20 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
21 | WebHost.CreateDefaultBuilder(args)
22 | .UseStartup();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ASP.NET Core Diagnostic Scenarios
2 |
3 | The goal of this repository is to show problematic application patterns for ASP.NET Core applications and a walk through on how to solve those issues.
4 | It shall serve as a collection of knowledge from real life application issues our customers have encountered.
5 |
6 | ## Common Pitfalls writing scalable services in ASP.NET Core
7 |
8 | Next you can find some guides for writing scalable services in ASP.NET Core. Some of the guidance is general purpose but will be explained through the lens of writing web services.
9 |
10 | - [General ASP.NET Core](AspNetCoreGuidance.md)
11 | - [Asynchronous Programming](AsyncGuidance.md)
12 |
13 | *NOTE:* The examples shown here are based on experiences with customer applications and issues found on Github and Stack Overflow.
14 |
--------------------------------------------------------------------------------
/Scenarios/Controllers/EmphemeralOperationController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Mvc;
6 | using Scenarios.Services;
7 |
8 | // For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
9 |
10 | namespace Scenarios.Controllers
11 | {
12 | public class EmphemeralOperationController : Controller
13 | {
14 | [HttpGet("/timer-1")]
15 | public IActionResult TimerLeak()
16 | {
17 | var operation = new EphemeralOperation();
18 | return Ok();
19 | }
20 |
21 | [HttpGet("/timer-2")]
22 | public IActionResult TimeLeakFix()
23 | {
24 | var operation = new EphemeralOperation2();
25 | return Ok();
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.doc diff=astextplain
2 | *.DOC diff=astextplain
3 | *.docx diff=astextplain
4 | *.DOCX diff=astextplain
5 | *.dot diff=astextplain
6 | *.DOT diff=astextplain
7 | *.pdf diff=astextplain
8 | *.PDF diff=astextplain
9 | *.rtf diff=astextplain
10 | *.RTF diff=astextplain
11 |
12 | *.jpg binary
13 | *.png binary
14 | *.gif binary
15 |
16 | *.cs text=auto diff=csharp
17 | *.vb text=auto
18 | *.resx text=auto
19 | *.c text=auto
20 | *.cpp text=auto
21 | *.cxx text=auto
22 | *.h text=auto
23 | *.hxx text=auto
24 | *.py text=auto
25 | *.rb text=auto
26 | *.java text=auto
27 | *.html text=auto
28 | *.htm text=auto
29 | *.css text=auto
30 | *.scss text=auto
31 | *.sass text=auto
32 | *.less text=auto
33 | *.js text=auto
34 | *.lisp text=auto
35 | *.clj text=auto
36 | *.sql text=auto
37 | *.php text=auto
38 | *.lua text=auto
39 | *.m text=auto
40 | *.asm text=auto
41 | *.erl text=auto
42 | *.fs text=auto
43 | *.fsx text=auto
44 | *.hs text=auto
45 |
46 | *.csproj text=auto
47 | *.vbproj text=auto
48 | *.fsproj text=auto
49 | *.dbproj text=auto
50 | *.sln text=auto eol=crlf
51 |
52 | *.sh eol=lf
53 |
--------------------------------------------------------------------------------
/AspNetCoreDiagnosticScenarios.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.28010.2036
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scenarios", "Scenarios\Scenarios.csproj", "{D05E3242-E5ED-493F-B75D-48A494C6CE4B}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {D05E3242-E5ED-493F-B75D-48A494C6CE4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {D05E3242-E5ED-493F-B75D-48A494C6CE4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {D05E3242-E5ED-493F-B75D-48A494C6CE4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {D05E3242-E5ED-493F-B75D-48A494C6CE4B}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {F02E06C8-1856-4E32-A48C-3C827878CD3A}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/Scenarios/Controllers/HttpClientController.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Net.Http;
3 | using System.Threading.Tasks;
4 | using Microsoft.AspNetCore.Mvc;
5 |
6 | namespace Scenarios.Controllers
7 | {
8 | public class HttpClientController : Controller
9 | {
10 | private readonly string _url = "https://raw.githubusercontent.com/Biuni/PokemonGO-Pokedex/master/pokedex.json";
11 |
12 | [Route("/httpclient-1")]
13 | public async Task OutgoingWorse()
14 | {
15 | var client = new HttpClient();
16 | return await client.GetStringAsync(_url);
17 | }
18 |
19 | [Route("/httpclient-2")]
20 | public async Task OutgoingBad()
21 | {
22 | using (var client = new HttpClient())
23 | {
24 | return await client.GetStringAsync(_url);
25 | }
26 | }
27 |
28 | [Route("/httpclient-3")]
29 | public async Task OutgoingGood([FromServices]IHttpClientFactory clientFactory)
30 | {
31 | using (var client = clientFactory.CreateClient())
32 | {
33 | return await client.GetStreamAsync(_url);
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Scenarios/Model/PokemonData.cs:
--------------------------------------------------------------------------------
1 | namespace Scenarios.Model
2 | {
3 | public class PokemonData
4 | {
5 | public Pokemon[] pokemon { get; set; }
6 | }
7 |
8 | public class Pokemon
9 | {
10 | public int id { get; set; }
11 | public string num { get; set; }
12 | public string name { get; set; }
13 | public string img { get; set; }
14 | public string[] type { get; set; }
15 | public string height { get; set; }
16 | public string weight { get; set; }
17 | public string candy { get; set; }
18 | public int candy_count { get; set; }
19 | public string egg { get; set; }
20 | public float spawn_chance { get; set; }
21 | public float avg_spawns { get; set; }
22 | public string spawn_time { get; set; }
23 | public float[] multipliers { get; set; }
24 | public string[] weaknesses { get; set; }
25 | public Next_Evolution[] next_evolution { get; set; }
26 | public Prev_Evolution[] prev_evolution { get; set; }
27 | }
28 |
29 | public class Next_Evolution
30 | {
31 | public string num { get; set; }
32 | public string name { get; set; }
33 | }
34 |
35 | public class Prev_Evolution
36 | {
37 | public string num { get; set; }
38 | public string name { get; set; }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Scenarios/PokemonDbContext.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using Microsoft.EntityFrameworkCore;
3 | using Newtonsoft.Json;
4 | using Scenarios.Model;
5 |
6 | namespace Scenarios
7 | {
8 | public class PokemonDbContext : DbContext
9 | {
10 | public PokemonDbContext(DbContextOptions options)
11 | : base(options)
12 | {
13 |
14 | }
15 |
16 | public DbSet Pokemon { get; set; }
17 |
18 | protected override void OnModelCreating(ModelBuilder modelBuilder)
19 | {
20 | PokemonData pokemonData;
21 | using (var stream = File.OpenRead("pokemon.json"))
22 | using (var streamReader = new StreamReader(stream))
23 | using (var reader = new JsonTextReader(streamReader))
24 | {
25 | var serializer = new JsonSerializer();
26 | pokemonData = serializer.Deserialize(reader);
27 | }
28 |
29 | modelBuilder.Entity()
30 | .HasData(pokemonData.pokemon);
31 |
32 | modelBuilder.Entity()
33 | .Ignore(p => p.next_evolution)
34 | .Ignore(p => p.multipliers)
35 | .Ignore(p => p.prev_evolution)
36 | .Ignore(p => p.weaknesses)
37 | .Ignore(p => p.type);
38 |
39 | base.OnModelCreating(modelBuilder);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Scenarios/Controllers/BigJsonOutputController.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Scenarios.Services;
4 |
5 | namespace Scenarios.Controllers
6 | {
7 | public class BigJsonOutputController : Controller
8 | {
9 | private readonly PokemonService _pokemonService;
10 | public BigJsonOutputController(PokemonService pokemonService)
11 | {
12 | _pokemonService = pokemonService;
13 | }
14 |
15 | [HttpGet("/big-json-content-1")]
16 | public async Task BigContentJsonBad()
17 | {
18 | var obj = await _pokemonService.GetPokemonBufferdStringAsync();
19 |
20 | return Ok(obj);
21 | }
22 |
23 | [HttpGet("/big-json-content-2")]
24 | public async Task BigContentJsonGood()
25 | {
26 | var obj = await _pokemonService.GetPokemonAsync();
27 |
28 | return Ok(obj);
29 | }
30 |
31 | [HttpGet("/big-json-content-3")]
32 | public async Task BigContentJsonManualUnbufferedBad()
33 | {
34 | var obj = await _pokemonService.GetPokemonManualUnbufferedBadAsync();
35 |
36 | return Ok(obj);
37 | }
38 |
39 | [HttpGet("/big-json-content-4")]
40 | public async Task BigContentJsonManualUnbufferedGood()
41 | {
42 | var obj = await _pokemonService.GetPokemonManualUnbufferedGoodAsync();
43 | return Ok(obj);
44 | }
45 |
46 | [HttpGet("/big-json-content-5")]
47 | public async Task BigContentJsonManualBuffered()
48 | {
49 | var obj = await _pokemonService.GetPokemonManualBufferedAsync();
50 | return Ok(obj);
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Scenarios/Controllers/AsyncVoidController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.WebSockets;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Http;
6 | using Microsoft.AspNetCore.Mvc;
7 |
8 | namespace Scenarios.Controllers
9 | {
10 | public class AsyncVoidController : Controller
11 | {
12 | [HttpGet("/async-void-1")]
13 | public async void Get()
14 | {
15 | await Task.Delay(1000);
16 |
17 | // THIS will crash the process since we're writing after the response has completed on a background thread
18 | await Response.WriteAsync("Hello World");
19 | }
20 |
21 | [HttpGet("/async-void-2")]
22 | public async Task BrokenWebSockets()
23 | {
24 | if (HttpContext.WebSockets.IsWebSocketRequest)
25 | {
26 | var ws = await HttpContext.WebSockets.AcceptWebSocketAsync();
27 |
28 | // This is broken because we're not holding the request open until the WebSocket is closed!
29 | _ = Echo(ws);
30 | }
31 | }
32 |
33 | private async Task Echo(WebSocket webSocket)
34 | {
35 | var buffer = new byte[1024 * 4];
36 | var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);
37 | while (!result.CloseStatus.HasValue)
38 | {
39 | await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);
40 |
41 | result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);
42 | }
43 | await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Scenarios/Controllers/CancelLegacyOperationController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Microsoft.AspNetCore.Mvc;
5 | using Scenarios.Services;
6 |
7 | namespace Scenarios.Controllers
8 | {
9 | public class CancelLegacyOperationController : Controller
10 | {
11 | [HttpGet("/legacy-cancellation-1")]
12 | public async Task LegacyCancellationWithTimeoutBad()
13 | {
14 | var service = new LegacyService();
15 | var timeout = TimeSpan.FromSeconds(10);
16 |
17 | var result = await service.DoAsyncOperation().TimeoutAfterBad(timeout);
18 |
19 | return Ok(result);
20 | }
21 |
22 | [HttpGet("/legacy-cancellation-2")]
23 | public async Task LegacyCancellationWithTimeoutGood()
24 | {
25 | var service = new LegacyService();
26 | var timeout = TimeSpan.FromSeconds(10);
27 |
28 | var result = await service.DoAsyncOperation().TimeoutAfter(timeout);
29 |
30 | return Ok(result);
31 | }
32 |
33 | [HttpGet("/legacy-cancellation-3")]
34 | public async Task LegacyCancellationWithCancellationBad()
35 | {
36 | var service = new LegacyService();
37 |
38 | var result = await service.DoAsyncOperation().WithCancellationBad(HttpContext.RequestAborted);
39 |
40 | return Ok(result);
41 | }
42 |
43 | [HttpGet("/legacy-cancellation-4")]
44 | public async Task LegacyCancellationWithCancellationGood()
45 | {
46 | var service = new LegacyService();
47 |
48 | var result = await service.DoAsyncOperation().WithCancellation(HttpContext.RequestAborted);
49 |
50 | return Ok(result);
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Scenarios/Controllers/CancelAsyncOperationController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Net.Http;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore.Mvc;
7 |
8 | namespace Scenarios.Controllers
9 | {
10 | public class CancelAsyncOperationController : Controller
11 | {
12 | private readonly string _url = "https://raw.githubusercontent.com/Biuni/PokemonGO-Pokedex/master/pokedex.json";
13 |
14 | [HttpGet("/cancellation-1")]
15 | public async Task HttpClientAsyncWithCancellationBad([FromServices]IHttpClientFactory clientFactory)
16 | {
17 | var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
18 |
19 | using (var client = clientFactory.CreateClient())
20 | {
21 | var response = await client.GetAsync(_url, cts.Token);
22 | return await response.Content.ReadAsStreamAsync();
23 | }
24 | }
25 |
26 | [HttpGet("/cancellation-2")]
27 | public async Task HttpClientAsyncWithCancellationBetter([FromServices]IHttpClientFactory clientFactory)
28 | {
29 | using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
30 | {
31 | using (var client = clientFactory.CreateClient())
32 | {
33 | var response = await client.GetAsync(_url, cts.Token);
34 | return await response.Content.ReadAsStreamAsync();
35 | }
36 | }
37 | }
38 |
39 | [HttpGet("/cancellation-3")]
40 | public async Task HttpClientAsyncWithCancellationBest([FromServices]IHttpClientFactory clientFactory)
41 | {
42 | // This has the timeout configured in Startup
43 | using (var client = clientFactory.CreateClient("timeout"))
44 | {
45 | return await client.GetStreamAsync(_url);
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Scenarios/Services/EphemeralOperation.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 |
4 | namespace Scenarios.Services
5 | {
6 | ///
7 | /// Creating this object without disposing it will cause a memory leak. The object graph will look like this
8 | /// Timer -> TimerHolder -> TimerQueueTimer -> EphemeralOperation -> Timer -> ...
9 | /// The timer holds onto the state which in turns holds onto the Timer, we have a circular reference.
10 | /// The GC will not clean these up even if it is unreferenced. It needs to be explicitly disposed in order to avoid the leak.
11 | ///
12 | public class EphemeralOperation : IDisposable
13 | {
14 | private Timer _timer;
15 | private int _ticks;
16 |
17 | public EphemeralOperation()
18 | {
19 | _timer = new Timer(state =>
20 | {
21 | _ticks++;
22 | },
23 | null,
24 | 1000,
25 | 1000);
26 | }
27 |
28 | public void Dispose()
29 | {
30 | _timer.Dispose();
31 | }
32 | }
33 |
34 | ///
35 | /// This fixes the cycle by using a WeakReference to the state object. The object graph now looks like this:
36 | /// Timer -> TimerHolder -> TimerQueueTimer -> WeakReference<EphemeralOperation> -> Timer -> ...
37 | /// If EphemeralOperation2 falls out of scope, the timer should be released.
38 | ///
39 | public class EphemeralOperation2 : IDisposable
40 | {
41 | private Timer _timer;
42 | private int _ticks;
43 |
44 | public EphemeralOperation2()
45 | {
46 | _timer = new Timer(OnTimerCallback,
47 | new WeakReference(this),
48 | 1000,
49 | 1000);
50 | }
51 |
52 | private static void OnTimerCallback(object state)
53 | {
54 | var thisRef = (WeakReference)state;
55 | if (thisRef.TryGetTarget(out var op))
56 | {
57 | op._ticks++;
58 | }
59 | }
60 |
61 | public void Dispose()
62 | {
63 | _timer.Dispose();
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Scenarios/Controllers/AsyncFactoryController.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Scenarios.Services;
4 |
5 | namespace Scenarios.Controllers
6 | {
7 | public class AsyncFactoryController : Controller
8 | {
9 | ///
10 | /// This action is problematic because it tries to resolve the remote connection from the DI container
11 | /// which requires an asynchronous operation. See Startup.cs for the registration of RemoteConnection.
12 | ///
13 | [HttpGet("/async-di-1")]
14 | public async Task PublishAsync([FromServices]RemoteConnection remoteConnection)
15 | {
16 | await remoteConnection.PublishAsync("group", "hello");
17 |
18 | return Accepted();
19 | }
20 |
21 | [HttpGet("/async-di-2")]
22 | public async Task PublishAsync([FromServices]RemoteConnectionFactory remoteConnectionFactory)
23 | {
24 | // This doesn't have the dead lock issue but it makes a new connection every time
25 | var connection = await remoteConnectionFactory.ConnectAsync();
26 |
27 | await connection.PublishAsync("group", "hello");
28 |
29 | // Dispose the connection we created
30 | await connection.DisposeAsync();
31 |
32 | return Accepted();
33 | }
34 |
35 | [HttpGet("/async-di-3")]
36 | public async Task PublishAsync([FromServices]LoggingRemoteConnection remoteConnection)
37 | {
38 | // This doesn't have the dead lock issue but it makes a new connection every time
39 | await remoteConnection.PublishAsync("group", "hello");
40 |
41 | return Accepted();
42 | }
43 |
44 | ///
45 | /// This is the cleanest pattern for dealing with async construction. The implementation of the connection is a bit
46 | /// more complicated but consumption looks like the first method that takes RemoteConnection and it is actually safe.
47 | ///
48 | [HttpGet("/async-di-4")]
49 | public async Task PublishAsync([FromServices]LazyRemoteConnection remoteConnection)
50 | {
51 | await remoteConnection.PublishAsync("group", "hello");
52 |
53 | return Accepted();
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Scenarios/Controllers/BigJsonInputController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Http;
6 | using Microsoft.AspNetCore.Mvc;
7 | using Microsoft.AspNetCore.WebUtilities;
8 | using Newtonsoft.Json;
9 | using Newtonsoft.Json.Linq;
10 | using Scenarios.Model;
11 |
12 | namespace Scenarios.Controllers
13 | {
14 | public class BigJsonInputController : Controller
15 | {
16 | [HttpPost("/big-json-input-1")]
17 | public IActionResult BigJsonSynchronousInput()
18 | {
19 | // This synchronously reads the entire http request body into memory and it has several problems:
20 | // 1. If the request is large it could lead to out of memory problems which can result in a Denial Of Service.
21 | // 2. If the client is slowly uploading, we're doing sync over async because Kestrel does *NOT* support synchronous reads.
22 | var json = new StreamReader(Request.Body).ReadToEnd();
23 |
24 | var rootobject = JsonConvert.DeserializeObject(json);
25 |
26 | return Accepted();
27 | }
28 |
29 | ///
30 | /// This uses MVC's built in model binding to create the PokemonData object. This is the most preferred approach as it handles all of the
31 | /// correct buffering on your behalf.
32 | ///
33 | [HttpPost("/big-json-input-2")]
34 | public IActionResult BigJsonInput([FromBody]PokemonData rootobject)
35 | {
36 | return Accepted();
37 | }
38 |
39 | [HttpPost("/big-json-input-3")]
40 | public async Task BigContentBad()
41 | {
42 | // This asynchronously reads the entire http request body into memory. It still suffers from the Denial Of Service
43 | // issue if the request body is too large but there's no threading issue.
44 | var json = await new StreamReader(Request.Body).ReadToEndAsync();
45 |
46 | var rootobject = JsonConvert.DeserializeObject(json);
47 |
48 | return Accepted();
49 | }
50 |
51 | [HttpPost("/big-json-input-4")]
52 | public async Task BigContentManualGood()
53 | {
54 | var streamReader = new HttpRequestStreamReader(Request.Body, Encoding.UTF8);
55 |
56 | var jsonReader = new JsonTextReader(streamReader);
57 | var serializer = new JsonSerializer();
58 |
59 | // This asynchronously reads the entire payload into a JObject then turns it into the real object.
60 | var obj = await JToken.ReadFromAsync(jsonReader);
61 | var rootobject = obj.ToObject(serializer);
62 |
63 | return Accepted();
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Scenarios/wwwroot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Scenario: Retrieving and returning JSON response from an external API
9 |
16 |
17 | Scenario: HTTP client to retrive a JSON response
18 |
23 |
24 | Scenario: Big JSON POST
25 |
31 |
32 | Scenario: HTTP client request with cancellation
33 |
38 |
39 | Scenario: Cancelling an async operation without native cancellation support
40 |
46 |
47 | Scenario: Executing an async operation in business logic
48 |
61 |
62 | Scenario: Fire and forget database operation
63 |
70 |
71 | Scenario: Async void in Controllers
72 |
76 |
77 |
--------------------------------------------------------------------------------
/Scenarios/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.AspNetCore.Builder;
4 | using Microsoft.AspNetCore.Hosting;
5 | using Microsoft.EntityFrameworkCore;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Microsoft.Extensions.Logging;
8 | using Scenarios.Services;
9 |
10 | namespace Scenarios
11 | {
12 | public class Startup
13 | {
14 | public void ConfigureServices(IServiceCollection services)
15 | {
16 | services.AddHttpClient();
17 |
18 | services.AddHttpClient();
19 |
20 | services.AddHttpClient("timeout", client =>
21 | {
22 | client.Timeout = TimeSpan.FromSeconds(10);
23 | });
24 |
25 | services.AddHttpContextAccessor();
26 |
27 | services.AddDbContext(o =>
28 | {
29 | o.UseInMemoryDatabase("MyApplication");
30 | });
31 |
32 | services.AddSingleton(sp =>
33 | {
34 | // This is *BAD*, do not try to do asynchronous work in a synchronous callback!
35 | // This can lead to thread pool starvation.
36 | return sp.GetRequiredService().ConnectAsync().Result;
37 | });
38 |
39 | services.AddSingleton(sp =>
40 | {
41 | // This is *BAD*, do not try to do asynchronous work in a synchronous callback!
42 | // This specific implementation can lead to a dead lock
43 | return GetLoggingRemoteConnection(sp).Result;
44 | });
45 |
46 | services.AddSingleton();
47 | services.AddSingleton();
48 |
49 | services.AddMvc();
50 | }
51 |
52 | private async Task GetLoggingRemoteConnection(IServiceProvider sp)
53 | {
54 | // As part of service resolution, we hold a lock on the container
55 | var connectionFactory = sp.GetRequiredService();
56 | var connection = await connectionFactory.ConnectAsync();
57 |
58 | // We've resumed on a different thread and we're about to trigger another call to GetRequiredService.
59 | // This call requires the same lock so it will wait for the existing service resolution to release it
60 | // before continuing.
61 |
62 | // This will result in a dead lock because we're running as part of the original service resolution!
63 | var logger = sp.GetRequiredService>();
64 |
65 | return new LoggingRemoteConnection(connection, logger);
66 | }
67 |
68 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
69 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, PokemonDbContext context)
70 | {
71 | if (env.IsDevelopment())
72 | {
73 | app.UseDeveloperExceptionPage();
74 | }
75 |
76 | app.UseFileServer();
77 |
78 | app.UseMvc();
79 |
80 | // Force database seeding to execute
81 | context.Database.EnsureCreated();
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Scenarios/Infrastructure/TaskExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace System.Threading.Tasks
2 | {
3 | public static class TaskExtensions
4 | {
5 | ///
6 | /// The timer won't be disposed until this token triggers. If it is a long lived token
7 | /// that may result in memory leaks and timer queue exhaustion!
8 | ///
9 | public static async Task WithCancellationBad(this Task task, CancellationToken cancellationToken)
10 | {
11 | var delayTask = Task.Delay(-1, cancellationToken);
12 |
13 | var resultTask = await Task.WhenAny(task, delayTask);
14 | if (resultTask == delayTask)
15 | {
16 | // Operation cancelled
17 | throw new OperationCanceledException();
18 | }
19 |
20 | return await task;
21 | }
22 |
23 | ///
24 | /// This properly registers and unregisters the token when one of the operations completes
25 | ///
26 | public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
27 | {
28 | var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
29 |
30 | // This disposes the registration as soon as one of the tasks trigger
31 | using (cancellationToken.Register(state =>
32 | {
33 | ((TaskCompletionSource)state).TrySetResult(null);
34 | },
35 | tcs))
36 | {
37 | var resultTask = await Task.WhenAny(task, tcs.Task);
38 | if (resultTask == tcs.Task)
39 | {
40 | // Operation cancelled
41 | throw new OperationCanceledException(cancellationToken);
42 | }
43 |
44 | return await task;
45 | }
46 | }
47 |
48 | ///
49 | /// This method does not cancel the timer even if the operation successfuly completes.
50 | /// This means you could end up with timer queue flooding!
51 | ///
52 | public static async Task TimeoutAfterBad(this Task task, TimeSpan timeout)
53 | {
54 | var delayTask = Task.Delay(timeout);
55 |
56 | var resultTask = await Task.WhenAny(task, delayTask);
57 | if (resultTask == delayTask)
58 | {
59 | // Operation cancelled
60 | throw new OperationCanceledException();
61 | }
62 |
63 | return await task;
64 | }
65 |
66 | ///
67 | /// This method cancels the timer if the operation succesfully completes.
68 | ///
69 | public static async Task TimeoutAfter(this Task task, TimeSpan timeout)
70 | {
71 | using (var cts = new CancellationTokenSource())
72 | {
73 | var delayTask = Task.Delay(timeout, cts.Token);
74 |
75 | var resultTask = await Task.WhenAny(task, delayTask);
76 | if (resultTask == delayTask)
77 | {
78 | // Operation cancelled
79 | throw new OperationCanceledException();
80 | }
81 | else
82 | {
83 | // Cancel the timer task so that it does not fire
84 | cts.Cancel();
85 | }
86 |
87 | return await task;
88 | }
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Scenarios/Controllers/AsyncOperationController.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 | using Microsoft.AspNetCore.Mvc;
4 | using Scenarios.Services;
5 |
6 | namespace Scenarios.Controllers
7 | {
8 | ///
9 | /// This controller shows to various ways people attempt to blocking code over an async API. There is no
10 | /// good way to turn asynchronous code into synchronous code. All of these blocking calls can cause thread pool starvation.
11 | ///
12 | public class AsyncOperationController : Controller
13 | {
14 | [HttpGet("/async-1")]
15 | public IActionResult BadBlocking1()
16 | {
17 | var service = new LegacyService();
18 |
19 | var result = service.DoOperationBlocking();
20 |
21 | return Ok(result);
22 | }
23 |
24 | [HttpGet("/async-2")]
25 | public IActionResult BadBlocking2()
26 | {
27 | var service = new LegacyService();
28 |
29 | var result = service.DoOperationBlocking2();
30 |
31 | return Ok(result);
32 | }
33 |
34 | [HttpGet("/async-3")]
35 | public IActionResult BadBlocking3()
36 | {
37 | var service = new LegacyService();
38 |
39 | var result = service.DoOperationBlocking3();
40 |
41 | return Ok(result);
42 | }
43 |
44 | [HttpGet("/async-4")]
45 | public IActionResult BadBlocking4()
46 | {
47 | var service = new LegacyService();
48 |
49 | var result = service.DoOperationBlocking4();
50 |
51 | return Ok(result);
52 | }
53 |
54 | [HttpGet("/async-5")]
55 | public IActionResult BadBlocking5()
56 | {
57 | var service = new LegacyService();
58 |
59 | var result = service.DoOperationBlocking5();
60 |
61 | return Ok(result);
62 | }
63 |
64 | [HttpGet("/async-6")]
65 | public IActionResult BadBlocking6()
66 | {
67 | var service = new LegacyService();
68 |
69 | var result = service.DoOperationBlocking6();
70 |
71 | return Ok(result);
72 | }
73 |
74 | [HttpGet("/async-7")]
75 | public IActionResult BadBlocking7()
76 | {
77 | var service = new LegacyService();
78 |
79 | var result = service.DoOperationBlocking7();
80 |
81 | return Ok(result);
82 | }
83 |
84 | [HttpGet("/async-8")]
85 | public async Task BadBlocking8()
86 | {
87 | var service = new LegacyService();
88 |
89 | var result = await service.DoAsyncOverSyncOperation();
90 |
91 | return Ok(result);
92 | }
93 |
94 | ///
95 | /// DoSyncOperationWithAsyncReturn has an async API over a synchronous call.
96 | ///
97 | [HttpGet("/async-9")]
98 | public async Task GoodBlocking()
99 | {
100 | var service = new LegacyService();
101 |
102 | var result = await service.DoSyncOperationWithAsyncReturn();
103 |
104 | return Ok(result);
105 | }
106 |
107 | [HttpGet("/async-10")]
108 | public async Task AsyncCall()
109 | {
110 | var service = new LegacyService();
111 |
112 | var result = await service.DoAsyncOperation();
113 |
114 | return Ok(result);
115 | }
116 |
117 | [HttpGet("/async-11")]
118 | public async Task AsyncCallLegacyBad()
119 | {
120 | var service = new LegacyService();
121 |
122 | var result = await service.DoAsyncOperationOverLegacyBad(HttpContext.RequestAborted);
123 |
124 | return Ok(result);
125 | }
126 |
127 | [HttpGet("/async-12")]
128 | public async Task AsyncCallLegacyGood()
129 | {
130 | var service = new LegacyService();
131 |
132 | var result = await service.DoAsyncOperationOverLegacy(HttpContext.RequestAborted);
133 |
134 | return Ok(result);
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/Scenarios/Services/RemoteConnection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.Logging;
5 |
6 | namespace Scenarios.Services
7 | {
8 | ///
9 | /// This represents a remote connection to something. Unfortunately Dispose is still problematic but that will be fixed with
10 | /// IAsyncDisposable. In the mean time, to dispose something on shutdown, we'd need to block in the dispose implementation (but that happens off request threads).
11 | ///
12 | public interface IRemoteConnection
13 | {
14 | Task PublishAsync(string channel, string message);
15 | Task DisposeAsync();
16 | }
17 |
18 | public class RemoteConnection : IRemoteConnection
19 | {
20 | public Task PublishAsync(string channel, string message)
21 | {
22 | return Task.CompletedTask;
23 | }
24 |
25 | public Task DisposeAsync() => Task.CompletedTask;
26 | }
27 |
28 | public class RemoteConnectionFactory
29 | {
30 | // Configurtion would be used to read the connection information
31 | private readonly IConfiguration _configuration;
32 |
33 | public RemoteConnectionFactory(IConfiguration configuration)
34 | {
35 | _configuration = configuration;
36 | }
37 |
38 | ///
39 | /// This factory method creates a new connection every time after connecting to that remote end point
40 | ///
41 | public async Task ConnectAsync()
42 | {
43 | var random = new Random();
44 |
45 | // Fake delay to a remote connection
46 | await Task.Delay(random.Next(10) * 1000);
47 |
48 | return new RemoteConnection();
49 | }
50 | }
51 |
52 | ///
53 | /// This implementation uses the RemoteConnectionFactory and lazily initializes the connection when operations happen
54 | ///
55 | public class LazyRemoteConnection : IRemoteConnection
56 | {
57 | private readonly AsyncLazy _connectionTask;
58 |
59 | public LazyRemoteConnection(RemoteConnectionFactory remoteConnectionFactory)
60 | {
61 | _connectionTask = new AsyncLazy(() => remoteConnectionFactory.ConnectAsync());
62 | }
63 |
64 | public async Task PublishAsync(string channel, string message)
65 | {
66 | var connection = await _connectionTask.Value;
67 |
68 | await connection.PublishAsync(channel, message);
69 | }
70 |
71 | public async Task DisposeAsync()
72 | {
73 | // Don't connect just to dispose
74 | if (!_connectionTask.IsValueCreated)
75 | {
76 | return;
77 | }
78 |
79 | var connection = await _connectionTask.Value;
80 |
81 | await connection.DisposeAsync();
82 | }
83 |
84 | private class AsyncLazy : Lazy>
85 | {
86 | public AsyncLazy(Func> valueFactory) : base(valueFactory)
87 | {
88 | }
89 | }
90 | }
91 |
92 | ///
93 | /// This connection implementation gets an IRemoteConnection and an ILoggerFactory in the constructor.
94 | /// It will dead lock the DI resolution process because it will end up waiting on the same lock.
95 | ///
96 | public class LoggingRemoteConnection : IRemoteConnection
97 | {
98 | private readonly IRemoteConnection _remoteConnection;
99 | private readonly ILogger _logger;
100 | public LoggingRemoteConnection(IRemoteConnection connection, ILogger logger)
101 | {
102 | _remoteConnection = connection;
103 | _logger = logger;
104 | }
105 |
106 | public Task DisposeAsync()
107 | {
108 | _logger.LogInformation("Disposing the remote connection");
109 | return _remoteConnection.DisposeAsync();
110 | }
111 |
112 | public Task PublishAsync(string channel, string message)
113 | {
114 | _logger.LogInformation("Publishing message={message} to the remote connection on channel {channel}", message, channel);
115 | return _remoteConnection.PublishAsync(channel, message);
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/Scenarios/Services/PokemonService.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Net.Http;
3 | using System.Net.Http.Headers;
4 | using System.Threading.Tasks;
5 | using Newtonsoft.Json;
6 | using Newtonsoft.Json.Linq;
7 | using Scenarios.Model;
8 |
9 | namespace Scenarios.Services
10 | {
11 | ///
12 | /// This service shows the various ways to make an outgoing HTTP request to get a JSON payload. It shows the various tradeoffs involved in doing this. It
13 | /// uses JSON.NET to perform Deserialization. In general there are 3 approaches:
14 | /// 1. Buffer the response in memory before handing it to the JSON serializer. This could lead to out of memory exceptions which can lead to a Denial Of Service.
15 | /// 2. Stream the response and synchronously read from the stream. This can lead to thread pool starvation.
16 | /// 3. Stream the response and asynchronously read from the stream.
17 | ///
18 | public class PokemonService
19 | {
20 | private readonly HttpClient _client;
21 | private readonly string _url = "https://raw.githubusercontent.com/Biuni/PokemonGO-Pokedex/master/pokedex.json";
22 |
23 | public PokemonService(HttpClient client)
24 | {
25 | _client = client;
26 | }
27 |
28 | public async Task GetPokemonBufferdStringAsync()
29 | {
30 | // This service returns the entire JSON payload into memory before converting that into a JSON object
31 | var json = await _client.GetStringAsync(_url);
32 |
33 | return JsonConvert.DeserializeObject(json);
34 | }
35 |
36 | public async Task GetPokemonAsync()
37 | {
38 | var response = await _client.GetAsync(_url);
39 | // This is a hack to work around the fact that this JSON api returns text/plain
40 | response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
41 | // Use the built in methods to read the response as JSON
42 | return await response.Content.ReadAsAsync();
43 | }
44 |
45 | public async Task GetPokemonManualUnbufferedBadAsync()
46 | {
47 | // Using HttpCompletionOption.ResponseHeadersRead avoids buffering the entire response body into memory
48 | using (var response = await _client.GetAsync(_url, HttpCompletionOption.ResponseHeadersRead))
49 | {
50 | // Get the response stream
51 | var responseStream = await response.Content.ReadAsStreamAsync();
52 |
53 | // Create a StreamReader and JsonTextReader over that
54 | // This does some buffering but we're not double buffering
55 | var textReader = new StreamReader(responseStream);
56 | var reader = new JsonTextReader(textReader);
57 |
58 | var serializer = new JsonSerializer();
59 |
60 | // *THIS* is a problem, we're doing synchronous IO here over the Stream. If the back end is slow, this can result
61 | // in thread pool starvation.
62 | return serializer.Deserialize(reader);
63 | }
64 | }
65 |
66 | public async Task GetPokemonManualUnbufferedGoodAsync()
67 | {
68 | // Using HttpCompletionOption.ResponseHeadersRead avoids buffering the entire response body into memory
69 | using (var response = await _client.GetAsync(_url, HttpCompletionOption.ResponseHeadersRead))
70 | {
71 | // Get the response stream
72 | var responseStream = await response.Content.ReadAsStreamAsync();
73 |
74 | // Create a StreamReader and JsonTextReader over that
75 | // This does double buffering...
76 | var textReader = new StreamReader(responseStream);
77 | var reader = new JsonTextReader(textReader);
78 |
79 | var serializer = new JsonSerializer();
80 |
81 | // This asynchronously reads the JSON object into memory. This does true synchronous IO. The only downside is that we're
82 | // converting the object graph to an intermediate DOM before going to the object directly.
83 | var obj = await JToken.ReadFromAsync(reader);
84 |
85 | // Convert the JToken to an object
86 | return obj.ToObject(serializer);
87 | }
88 | }
89 |
90 | public async Task GetPokemonManualBufferedAsync()
91 | {
92 | // This buffers the entire response into memory so that we don't end up doing blocking IO when
93 | // de-serializing the JSON. If the payload is *HUGE* this could result in large allocations that lead to a Denial Of Service.
94 | using (var response = await _client.GetAsync(_url))
95 | {
96 | // Get the response stream
97 | var responseStream = await response.Content.ReadAsStreamAsync();
98 |
99 | // Create a StreamReader and JsonTextReader over that
100 | // This does double buffering...
101 | var textReader = new StreamReader(responseStream);
102 | var reader = new JsonTextReader(textReader);
103 |
104 | var serializer = new JsonSerializer();
105 |
106 | // Because we're buffering the entire response, we're also avoiding synchronous IO
107 | return serializer.Deserialize(reader);
108 | }
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Scenarios/Services/LegacyService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 |
5 | namespace Scenarios.Services
6 | {
7 | ///
8 | /// The shows to various ways people attempt to blocking code over an async API. There is NO
9 | /// good way to turn asynchronous code into synchronous code. All of these blocking calls can cause thread pool starvation.
10 | ///
11 | public class LegacyService
12 | {
13 | public string DoOperationBlocking()
14 | {
15 | return Task.Run(() => DoAsyncOperation()).Result;
16 | }
17 |
18 | public string DoOperationBlocking2()
19 | {
20 | return Task.Run(() => DoAsyncOperation()).GetAwaiter().GetResult();
21 | }
22 |
23 | public string DoOperationBlocking3()
24 | {
25 | return Task.Run(() => DoAsyncOperation().Result).Result;
26 | }
27 |
28 | public string DoOperationBlocking4()
29 | {
30 | return Task.Run(() => DoAsyncOperation().GetAwaiter().GetResult()).GetAwaiter().GetResult();
31 | }
32 |
33 | public string DoOperationBlocking5()
34 | {
35 | return DoAsyncOperation().Result;
36 | }
37 |
38 | public string DoOperationBlocking6()
39 | {
40 | return DoAsyncOperation().GetAwaiter().GetResult();
41 | }
42 |
43 | public string DoOperationBlocking7()
44 | {
45 | var task = DoAsyncOperation();
46 | task.Wait();
47 | return task.GetAwaiter().GetResult();
48 | }
49 |
50 | ///
51 | /// DoAsyncOperation is a truly async operation. This is the recommended way to do asynchronous calls.
52 | ///
53 | ///
54 | public async Task DoAsyncOperation()
55 | {
56 | var random = new Random();
57 |
58 | // Mimick some asynchrous activity
59 | await Task.Delay(random.Next(10) * 1000);
60 |
61 | return Guid.NewGuid().ToString();
62 | }
63 |
64 | ///
65 | /// DoAsyncOverSyncOperation is wasteful. It uses a thread pool thread to return an easily computed value.
66 | /// The preferred approach is DoSyncOperationWithAsyncReturn.
67 | ///
68 | ///
69 | public Task DoAsyncOverSyncOperation()
70 | {
71 | return Task.Run(() => Guid.NewGuid().ToString());
72 | }
73 |
74 | ///
75 | /// This is the recommended way to return a Task for an already computed result. There's no need to use a thread pool thread,
76 | /// just to return a Task.
77 | ///
78 | ///
79 | public Task DoSyncOperationWithAsyncReturn()
80 | {
81 | return Task.FromResult(Guid.NewGuid().ToString());
82 | }
83 |
84 | ///
85 | /// DoAsyncOperationOverLegacyBad shows an async operation that does not properly clean up references to the canceallation token.
86 | ///
87 | public Task DoAsyncOperationOverLegacyBad(CancellationToken cancellationToken)
88 | {
89 | // The following TaskCompletionSource hasn't been creating with the TaskCreationOptions.RunContinuationsAsynchronously
90 | // option. This means that the calling code will resume in the OnCompleted callback. This has a couple of consequences
91 | // 1. It will extend the lifetime of these objects since they will be on the stack when user code is resumed.
92 | // 2. If the calling code blocks, it could *steal* the thread from the LegacyAsyncOperation.
93 | var tcs = new TaskCompletionSource();
94 |
95 | var operation = new LegacyAsyncOperation();
96 |
97 | if (cancellationToken.CanBeCanceled)
98 | {
99 | // CancellationToken.Register returns a CancellationTokenRegistration that needs to be disposed.
100 | // If this isn't disposed, it will stay around in the CancellationTokenSource until the
101 | // backing CancellationTokenSource is disposed.
102 | cancellationToken.Register(state =>
103 | {
104 | ((LegacyAsyncOperation)state).Cancel();
105 | },
106 | operation);
107 | }
108 |
109 | // Not removing the event handler can result in a memory leak
110 | // this object is referenced by the callback which itself isn't cleaned up until
111 | // the token is disposed or the registration is disposed.
112 | operation.Completed += OnCompleted;
113 |
114 | operation.Start();
115 |
116 | return tcs.Task;
117 |
118 | void OnCompleted(string result, bool cancelled)
119 | {
120 | if (cancelled)
121 | {
122 | tcs.TrySetCanceled(cancellationToken);
123 | }
124 | else
125 | {
126 | tcs.TrySetResult(result);
127 | }
128 | }
129 | }
130 |
131 | public Task DoAsyncOperationOverLegacy(CancellationToken cancellationToken)
132 | {
133 | var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
134 |
135 | var operation = new LegacyAsyncOperation();
136 |
137 | var registration = default(CancellationTokenRegistration);
138 |
139 | if (cancellationToken.CanBeCanceled)
140 | {
141 | registration = cancellationToken.Register(state =>
142 | {
143 | ((LegacyAsyncOperation)state).Cancel();
144 | },
145 | operation);
146 | }
147 |
148 | operation.Completed += OnCompleted;
149 |
150 | operation.Start();
151 |
152 | return tcs.Task;
153 |
154 | void OnCompleted(string result, bool cancelled)
155 | {
156 | registration.Dispose();
157 |
158 | operation.Completed -= OnCompleted;
159 |
160 | if (cancelled)
161 | {
162 | tcs.TrySetCanceled(cancellationToken);
163 | }
164 | else
165 | {
166 | tcs.TrySetResult(result);
167 | }
168 | }
169 | }
170 |
171 | ///
172 | /// Pretends to be a legacy async operation that doesn't natively support Task
173 | ///
174 | private class LegacyAsyncOperation
175 | {
176 | private Timer _timer;
177 |
178 | public Action Completed;
179 |
180 | private bool _cancelled;
181 |
182 | public void Start()
183 | {
184 | _timer = new Timer(OnCompleted, null, new Random().Next(10) * 1000, Timeout.Infinite);
185 | }
186 |
187 | private void OnCompleted(object state)
188 | {
189 | var cancelled = _cancelled;
190 | _cancelled = false;
191 |
192 | Completed(Guid.NewGuid().ToString(), cancelled);
193 |
194 | _timer.Dispose();
195 | }
196 |
197 | public void Cancel()
198 | {
199 | _cancelled = true;
200 | }
201 | }
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/Scenarios/Controllers/FireAndForgetController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Microsoft.AspNetCore.Http;
5 | using Microsoft.AspNetCore.Mvc;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Microsoft.Extensions.Logging;
8 | using Scenarios.Model;
9 |
10 | namespace Scenarios.Controllers
11 | {
12 | public class FireAndForgetController : Controller
13 | {
14 | [HttpGet("/fire-and-forget-1")]
15 | public IActionResult FireAndForget1([FromServices]PokemonDbContext context)
16 | {
17 | // This is an implicit async void method. ThreadPool.QueueUserWorkItem takes an Action, but the compiler allows
18 | // async void delegates to be used in its place. This is dangerous because unhandled exceptions will bring down the entire server process.
19 | ThreadPool.QueueUserWorkItem(async state =>
20 | {
21 | await Task.Delay(1000);
22 |
23 | // This closure is capturing the context from the Controller action parameter. This is bad because this work item could run
24 | // outside of the request scope and the PokemonDbContext is scoped to the request. As a result, this will crash the process with
25 | // and ObjectDisposedException
26 | context.Pokemon.Add(new Pokemon());
27 | await context.SaveChangesAsync();
28 | });
29 |
30 | return Accepted();
31 | }
32 |
33 |
34 | [HttpGet("/fire-and-forget-2")]
35 | public IActionResult FireAndForget2([FromServices]PokemonDbContext context)
36 | {
37 | // This uses Task.Run instead of ThreadPool.QueueUserWorkItem. It's mostly equivalent to the FireAndForget1 but since we're using
38 | // async Task instead of async void, unhandled exceptions won't crash the process. They will however trigger the TaskScheduler.UnobservedTaskException
39 | // event when exceptions go unhandled.
40 | Task.Run(async () =>
41 | {
42 | await Task.Delay(1000);
43 |
44 | // This closure is capturing the context from the Controller action parameter. This is bad because this work item could run
45 | // outside of the request scope and the PokemonDbContext is scoped to the request. As a result, this will throw an unhandled ObjectDisposedException.
46 | context.Pokemon.Add(new Pokemon());
47 | await context.SaveChangesAsync();
48 | });
49 |
50 | return Accepted();
51 | }
52 |
53 | [HttpGet("/fire-and-forget-3")]
54 |
55 | public IActionResult FireAndForget3([FromServices]IServiceScopeFactory serviceScopeFactory)
56 | {
57 | // This version of fire and forget adds some exception handling. We're also no longer capturing the PokemonDbContext from the incoming request.
58 | // Instead, we're injecting an IServiceScopeFactory (which is a singleton) in order to create a scope in the background work item.
59 | Task.Run(async () =>
60 | {
61 | await Task.Delay(1000);
62 |
63 | // Create a scope for the lifetime of the background operation and resolve services from it
64 | using (var scope = serviceScopeFactory.CreateScope())
65 | {
66 | var loggerFactory = scope.ServiceProvider.GetRequiredService();
67 | var logger = loggerFactory.CreateLogger("Background Task");
68 |
69 | // THIS IS DANGEROUS! We're capturing the HttpContext from the incoming request in the closure that
70 | // runs the background work item. This will not work because the incoming http request will be over before
71 | // the work item executes.
72 | using (logger.BeginScope("Background operation kicked off from {RequestId}", HttpContext.TraceIdentifier))
73 | {
74 | try
75 | {
76 | // This will a PokemonDbContext from the correct scope and the operation will succeed
77 | var context = scope.ServiceProvider.GetRequiredService();
78 |
79 | context.Pokemon.Add(new Pokemon());
80 | await context.SaveChangesAsync();
81 | }
82 | catch (Exception ex)
83 | {
84 | logger.LogError(ex, "Background task failed.");
85 | }
86 | }
87 | }
88 | });
89 |
90 | return Accepted();
91 | }
92 |
93 | [HttpGet("/fire-and-forget-4")]
94 | public IActionResult FireAndForget4([FromServices]IServiceScopeFactory serviceScopeFactory)
95 | {
96 | Task.Run(async () =>
97 | {
98 | await Task.Delay(1000);
99 |
100 | using (var scope = serviceScopeFactory.CreateScope())
101 | {
102 | // Instead of capturing the HttpContext from the controller property, we use the IHttpContextAccessor
103 | var accessor = scope.ServiceProvider.GetRequiredService();
104 | var loggerFactory = scope.ServiceProvider.GetRequiredService();
105 |
106 | var logger = loggerFactory.CreateLogger("Background Task");
107 |
108 | // THIS IS DANGEROUS! We're trying to use the HttpContext from the incoming request in the closure that
109 | // runs the background work item. This will not work because the incoming http request will be over before
110 | // the work item executes.
111 | using (logger.BeginScope("Background operation kicked off from {RequestId}", accessor.HttpContext.TraceIdentifier))
112 | {
113 | try
114 | {
115 |
116 | var context = scope.ServiceProvider.GetRequiredService();
117 |
118 | context.Pokemon.Add(new Pokemon());
119 | await context.SaveChangesAsync();
120 | }
121 | catch (Exception ex)
122 | {
123 | logger.LogError(ex, "Background task failed.");
124 | }
125 | }
126 | }
127 | });
128 |
129 | return Accepted();
130 | }
131 |
132 | [HttpGet("/fire-and-forget-5")]
133 | public IActionResult FireAndForget5([FromServices]IServiceScopeFactory serviceScopeFactory)
134 | {
135 | // Capture the trace identifier first
136 | string traceIdenifier = HttpContext.TraceIdentifier;
137 |
138 | Task.Run(async () =>
139 | {
140 | await Task.Delay(1000);
141 |
142 | using (var scope = serviceScopeFactory.CreateScope())
143 | {
144 | var loggerFactory = scope.ServiceProvider.GetRequiredService();
145 |
146 | var logger = loggerFactory.CreateLogger("Background Task");
147 |
148 | // This uses the traceIdenifier captured at the time the request started.
149 | using (logger.BeginScope("Background operation kicked off from {RequestId}", traceIdenifier))
150 | {
151 | try
152 | {
153 |
154 | var context = scope.ServiceProvider.GetRequiredService();
155 |
156 | context.Add(new Pokemon());
157 | await context.SaveChangesAsync();
158 | }
159 | catch (Exception ex)
160 | {
161 | logger.LogError(ex, "Background task failed.");
162 | }
163 | }
164 | }
165 | });
166 |
167 | return Accepted();
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/AspNetCoreGuidance.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 | - [ASP.NET Core Guidance](#aspnet-core-guidance)
3 | - [Avoid using synchronous Read/Write overloads on HttpRequest.Body and HttpResponse.Body](#avoid-using-synchronous-readwrite-overloads-on-httprequestbody-and-httpresponsebody)
4 | - [Prefer using HttpRequest.ReadAsFormAsync() over HttpRequest.Form](#prefer-using-httprequestreadasformasync-over-httprequestform)
5 | - [Use buffered and synchronous reads and writes as an alternative to asynchronous reading and writing](#use-buffered-and-synchronous-reads-and-writes-as-an-alternative-to-asynchronous-reading-and-writing)
6 | - [Avoid reading large request bodies or response bodies into memory](#avoid-reading-large-request-bodies-or-response-bodies-into-memory)
7 | - [Do not store IHttpContextAccessor.HttpContext in a field](#do-not-store-ihttpcontextaccessorhttpcontext-in-a-field)
8 | - [Do not access the HttpContext from multiple threads in parallel. It is not thread safe.](#do-not-access-the-httpcontext-from-multiple-threads-in-parallel-it-is-not-thread-safe)
9 | - [Do not use the HttpContext after the request is complete](#do-not-use-the-httpcontext-after-the-request-is-complete)
10 | - [Do not capture the HttpContext in background threads](#do-not-capture-the-httpcontext-in-background-threads)
11 | - [Do not capture services injected into the controllers on background threads](#do-not-capture-services-injected-into-the-controllers-on-background-threads)
12 | - [Avoid adding headers after the HttpResponse has started](#avoid-adding-headers-after-the-httpresponse-has-started)
13 |
14 | # ASP.NET Core Guidance
15 |
16 | 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.
17 |
18 | ## Avoid using synchronous Read/Write overloads on HttpRequest.Body and HttpResponse.Body
19 |
20 | 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).
21 |
22 | ❌ **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](AsyncGuidance.md#avoid-using-taskresult-and-taskwait).
23 |
24 | ```C#
25 | public class MyController : Controller
26 | {
27 | [HttpGet("/pokemon")]
28 | public ActionResult Get()
29 | {
30 | // This synchronously reads the entire http request body into memory.
31 | // If the client is slowly uploading, we're doing sync over async because Kestrel does *NOT* support synchronous reads.
32 | var json = new StreamReader(Request.Body).ReadToEnd();
33 |
34 | return JsonConvert.DeserializeObject(json);
35 | }
36 | }
37 | ```
38 |
39 | :white_check_mark: **GOOD** This example uses `StreamReader.ReadToEndAsync` and as a result, does not block the thread while reading.
40 |
41 | ```C#
42 | public class MyController : Controller
43 | {
44 | [HttpGet("/pokemon")]
45 | public async Task> Get()
46 | {
47 | // This asynchronously reads the entire http request body into memory.
48 | var json = await new StreamReader(Request.Body).ReadToEndAsync();
49 |
50 | return JsonConvert.DeserializeObject(json);
51 | }
52 | }
53 | ```
54 |
55 | :bulb:**NOTE: If the request is large it could lead to out of memory problems which can result in a Denial Of Service. See [this](#avoid-reading-large-request-bodies-or-response-bodies-into-memory) for more information.**
56 |
57 | ## Prefer using HttpRequest.ReadAsFormAsync() over HttpRequest.Form
58 |
59 | You should always prefer `HttpRequest.ReadAsFormAsync()` 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.ReadAsFormAsync()` and the cached form value is being read using `HttpRequest.Form`.
60 |
61 | ❌ **BAD** This example uses HttpRequest.Form uses [sync over async](AsyncGuidance.md#avoid-using-taskresult-and-taskwait) under the covers and can lead to thread pool starvation (in some cases).
62 |
63 | ```C#
64 | public class MyController : Controller
65 | {
66 | [HttpPost("/form-body")]
67 | public IActionResult Post()
68 | {
69 | var form = HttpRequest.Form;
70 |
71 | Process(form["id"], form["name"]);
72 |
73 | return Accepted();
74 | }
75 | }
76 | ```
77 |
78 | :white_check_mark: **GOOD** This example uses `HttpRequest.ReadAsFormAsync()` to read the form body asynchronously.
79 |
80 | ```C#
81 | public class MyController : Controller
82 | {
83 | [HttpPost("/form-body")]
84 | public async Task Post()
85 | {
86 | var form = await HttpRequest.ReadAsFormAsync();
87 |
88 | Process(form["id"], form["name"]);
89 |
90 | return Accepted();
91 | }
92 | }
93 | ```
94 |
95 | ## Avoid reading large request bodies or response bodies into memory
96 |
97 | In .NET any single object allocation greater than 85KB ends up in the large object heap ([LOH](https://blogs.msdn.microsoft.com/maoni/2006/04/19/large-object-heap/)). Large objects are expensive in 2 ways:
98 |
99 | - 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)
100 | - LOH is collected with the rest of the heap (it requires a "full garbage collection" or Gen2 collection)
101 |
102 | This [blog post](https://adamsitnik.com/Array-Pool/#the-problem) describes the problem succinctly:
103 |
104 | > 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).
105 |
106 | 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.
107 |
108 | ## Use buffered and synchronous reads and writes as an alternative to asynchronous reading and writing
109 |
110 | 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.
111 |
112 | :bulb:**NOTE: If the request is large it could lead to out of memory problems which can result in a Denial Of Service. See [this](#avoid-reading-large-request-bodies-or-response-bodies-into-memory) for more information.**
113 |
114 | ## Do not store IHttpContextAccessor.HttpContext in a field
115 |
116 | 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.
117 |
118 | ❌ **BAD** This example stores the HttpContext in a field then attempts to use it later.
119 |
120 | ```C#
121 | public class MyType
122 | {
123 | private readonly HttpContext _context;
124 | public MyType(IHttpContextAccessor accessor)
125 | {
126 | _context = accessor.HttpContext;
127 | }
128 |
129 | public void CheckAdmin()
130 | {
131 | if (!_context.User.IsInRole("admin"))
132 | {
133 | throw new UnauthorizedAccessException("The current user isn't an admin");
134 | }
135 | }
136 | }
137 | ```
138 |
139 | The above logic will likely capture a null or bogus HttpContext in the constructor for later use.
140 |
141 | :white_check_mark: **GOOD** This example stores the IHttpContextAccesor itself in a field and uses the HttpContext field at the correct time (checking for null).
142 |
143 | ```C#
144 | public class MyType
145 | {
146 | private readonly IHttpContextAccessor _accessor;
147 | public MyType(IHttpContextAccessor accessor)
148 | {
149 | _accessor = accessor;
150 | }
151 |
152 | public void CheckAdmin()
153 | {
154 | var context = _accessor.HttpContext;
155 | if (context != null && !context.User.IsInRole("admin"))
156 | {
157 | throw new UnauthorizedAccessException("The current user isn't an admin");
158 | }
159 | }
160 | }
161 | ```
162 |
163 | ## Do not access the HttpContext from multiple threads in parallel. It is not thread safe.
164 |
165 | The `HttpContext` is *NOT* threadsafe. Accessing it from multiple threads in parallel can cause corruption resulting in undefined behavior (hangs, crashes, data corruption).
166 |
167 | ❌ **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.
168 |
169 | ```C#
170 | public class AsyncController : Controller
171 | {
172 | [HttpGet("/search")]
173 | public async Task Get(string query)
174 | {
175 | var query1 = SearchAsync(SearchEngine.Google, query);
176 | var query2 = SearchAsync(SearchEngine.Bing, query);
177 | var query3 = SearchAsync(SearchEngine.DuckDuckGo, query);
178 |
179 | await Task.WhenAll(query1, query2, query3);
180 |
181 | var results1 = await query1;
182 | var results2 = await query2;
183 | var results3 = await query3;
184 |
185 | return SearchResults.Combine(results1, results2, results3);
186 | }
187 |
188 | private async Task SearchAsync(SearchEngine engine, string query)
189 | {
190 | var searchResults = SearchResults.Empty;
191 | try
192 | {
193 | _logger.LogInformation("Starting search query from {path}.", HttpContext.Request.Path);
194 | searchResults = await _searchService.SearchAsync(engine, query);
195 | _logger.LogInformation("Finishing search query from {path}.", HttpContext.Request.Path);
196 | }
197 | catch (Exception ex)
198 | {
199 | _logger.LogError(ex, "Failed query from {path}", HttpContext.Request.Path);
200 | }
201 |
202 | return searchResults;
203 | }
204 | }
205 | ```
206 |
207 | :white_check_mark: **GOOD** This example copies all data from the incoming request before making the 3 parallel requests.
208 |
209 | ```C#
210 | public class AsyncController : Controller
211 | {
212 | [HttpGet("/search")]
213 | public async Task Get(string query)
214 | {
215 | string path = HttpContext.Request.Path;
216 | var query1 = SearchAsync(SearchEngine.Google, query, path);
217 | var query2 = SearchAsync(SearchEngine.Bing, query, path);
218 | var query3 = SearchAsync(SearchEngine.DuckDuckGo, query, path);
219 |
220 | await Task.WhenAll(query1, query2, query3);
221 |
222 | var results1 = await query1;
223 | var results2 = await query2;
224 | var results3 = await query3;
225 |
226 | return SearchResults.Combine(results1, results2, results3);
227 | }
228 |
229 | private async Task SearchAsync(SearchEngine engine, string query, string path)
230 | {
231 | var searchResults = SearchResults.Empty;
232 | try
233 | {
234 | _logger.LogInformation("Starting search query from {path}.", path);
235 | searchResults = await _searchService.SearchAsync(engine, query);
236 | _logger.LogInformation("Finishing search query from {path}.", path);
237 | }
238 | catch (Exception ex)
239 | {
240 | _logger.LogError(ex, "Failed query from {path}", path);
241 | }
242 |
243 | return searchResults;
244 | }
245 | }
246 | ```
247 |
248 | ## Do not use the HttpContext after the request is complete
249 |
250 | 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.
251 |
252 | ❌ **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.
253 |
254 | ```C#
255 | public class AsyncVoidController : Controller
256 | {
257 | [HttpGet("/async")]
258 | public async void Get()
259 | {
260 | await Task.Delay(1000);
261 |
262 | // THIS will crash the process since we're writing after the response has completed on a background thread
263 | await Response.WriteAsync("Hello World");
264 | }
265 | }
266 | ```
267 |
268 | :white_check_mark: **GOOD** This example returns a `Task` to the framework so the http request doesn't complete until the entire action completes.
269 |
270 | ```C#
271 | public class AsyncController : Controller
272 | {
273 | [HttpGet("/async")]
274 | public async Task Get()
275 | {
276 | await Task.Delay(1000);
277 |
278 | await Response.WriteAsync("Hello World");
279 | }
280 | }
281 | ```
282 |
283 | ## Do not capture the HttpContext in background threads
284 |
285 | ❌ **BAD** This example shows a closure is capturing the HttpContext from the Controller property. This is bad because this work item could run
286 | outside of the request scope and as a result, could lead to reading a bogus HttpContext.
287 |
288 | ```C#
289 | [HttpGet("/fire-and-forget-1")]
290 | public IActionResult FireAndForget1()
291 | {
292 | _ = Task.Run(() =>
293 | {
294 | await Task.Delay(1000);
295 |
296 | // This closure is capturing the context from the Controller property. This is bad because this work item could run
297 | // outside of the http request leading to reading of bogus data.
298 | var path = HttpContext.Request.Path;
299 | Log(path);
300 | });
301 |
302 | return Accepted();
303 | }
304 | ```
305 |
306 |
307 | :white_check_mark: **GOOD** This example copies the data required in the background task during the request explictly and does not reference
308 | anything from the controller itself.
309 |
310 | ```C#
311 | [HttpGet("/fire-and-forget-3")]
312 | public IActionResult FireAndForget3()
313 | {
314 | string path = HttpContext.Request.Path;
315 | _ = Task.Run(async () =>
316 | {
317 | await Task.Delay(1000);
318 |
319 | // This captures just the path
320 | Log(path);
321 | });
322 |
323 | return Accepted();
324 | }
325 | ```
326 |
327 | ## Do not capture services injected into the controllers on background threads
328 |
329 | ❌ **BAD** This example shows a closure is capturing the DbContext from the Controller action parameter. This is bad because this work item could run
330 | outside of the request scope and the PokemonDbContext is scoped to the request. As a result, this will end up with an ObjectDisposedException.
331 |
332 | ```C#
333 | [HttpGet("/fire-and-forget-1")]
334 | public IActionResult FireAndForget1([FromServices]PokemonDbContext context)
335 | {
336 | _ = Task.Run(() =>
337 | {
338 | await Task.Delay(1000);
339 |
340 | // This closure is capturing the context from the Controller action parameter. This is bad because this work item could run
341 | // outside of the request scope and the PokemonDbContext is scoped to the request. As a result, this throw an ObjectDisposedException
342 | context.Pokemon.Add(new Pokemon());
343 | await context.SaveChangesAsync();
344 | });
345 |
346 | return Accepted();
347 | }
348 | ```
349 |
350 | :white_check_mark: **GOOD** This example injects an `IServiceScopeFactory` and creates a new dependency injection scope in the background thread and does not reference
351 | anything from the controller itself.
352 |
353 | ```C#
354 | [HttpGet("/fire-and-forget-3")]
355 | public IActionResult FireAndForget3([FromServices]IServiceScopeFactory serviceScopeFactory)
356 | {
357 | // This version of fire and forget adds some exception handling. We're also no longer capturing the PokemonDbContext from the incoming request.
358 | // Instead, we're injecting an IServiceScopeFactory (which is a singleton) in order to create a scope in the background work item.
359 | _ = Task.Run(async () =>
360 | {
361 | await Task.Delay(1000);
362 |
363 | // Create a scope for the lifetime of the background operation and resolve services from it
364 | using (var scope = serviceScopeFactory.CreateScope())
365 | {
366 | // This will a PokemonDbContext from the correct scope and the operation will succeed
367 | var context = scope.ServiceProvider.GetRequiredService();
368 |
369 | context.Pokemon.Add(new Pokemon());
370 | await context.SaveChangesAsync();
371 | }
372 | });
373 |
374 | return Accepted();
375 | }
376 | ```
377 |
378 | ## Avoid adding headers after the HttpResponse has started
379 |
380 | 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.
381 |
382 | ❌ **BAD** This logic tries to add response headers after the response has already started.
383 |
384 | ```C#
385 | app.Use(async (next, context) =>
386 | {
387 | await context.Response.WriteAsync("Hello ");
388 |
389 | await next();
390 |
391 | // This may fail if next() already wrote to the response
392 | context.Response.Headers["test"] = "value";
393 | });
394 | ```
395 |
396 | :white_check_mark: **GOOD** This example checks if the http response has started before writing to the body.
397 |
398 | ```C#
399 | app.Use(async (next, context) =>
400 | {
401 | await context.Response.WriteAsync("Hello ");
402 |
403 | await next();
404 |
405 | // Check if the response has already started before adding header and writing
406 | if (!context.Response.HasStarted)
407 | {
408 | context.Response.Headers["test"] = "value";
409 | }
410 | });
411 | ```
412 |
413 | :white_check_mark: **GOOD** This examples uses `HttpResponse.OnStarting` to set the headers before the response headers are flushed to the client.
414 |
415 | 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.
416 |
417 | ```C#
418 | app.Use(async (next, context) =>
419 | {
420 | // Wire up the callback that will fire just before the response headers are sent to the client.
421 | context.Response.OnStarting(() =>
422 | {
423 | context.Response.Headers["someheader"] = "somevalue";
424 | return Task.CompletedTask;
425 | });
426 |
427 | await next();
428 | });
429 | ```
430 |
--------------------------------------------------------------------------------
/AsyncGuidance.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 | - [Asynchronous Programming](#asynchronous-programming)
3 | - [Asynchrony is viral](#asynchrony-is-viral)
4 | - [Async void](#async-void)
5 | - [Prefer Task.FromResult over Task.Run for pre-computed or trivially computed data](#prefer-taskfromresult-over-taskrun-for-pre-computed-or-trivially-computed-data)
6 | - [Avoid using Task.Run for long running work that blocks the thread](#avoid-using-taskrun-for-long-running-work-that-blocks-the-thread)
7 | - [Avoid using Task.Result and Task.Wait](#avoid-using-taskresult-and-taskwait)
8 | - [Prefer await over ContinueWith](#prefer-await-over-continuewith)
9 | - [Always create TaskCompletionSource\ with TaskCreationOptions.RunContinuationsAsynchronously](#always-create-taskcompletionsourcet-with-taskcreationoptionsruncontinuationsasynchronously)
10 | - [Always dispose CancellationTokenSource(s) used for timeouts](#always-dispose-cancellationtokensources-used-for-timeouts)
11 | - [Always flow CancellationToken(s) to APIs that take a CancellationToken](#always-flow-cancellationtokens-to-apis-that-take-a-cancellationtoken)
12 | - [Cancelling uncancellable operations](#cancelling-uncancellable-operations)
13 | - [Always call FlushAsync on StreamWriter(s) or Stream(s) before calling Dispose](#always-call-flushasync-on-streamwriters-or-streams-before-calling-dispose)
14 | - [Prefer async/await over directly returning Task](#prefer-asyncawait-over-directly-returning-task)
15 | - [ConfigureAwait](#configureawait)
16 | - [Scenarios](#scenarios)
17 | - [Timer callbacks](#timer-callbacks)
18 | - [Implicit async void delegates](#implicit-async-void-delegates)
19 | - [ConcurrentDictionary.GetOrAdd](#concurrentdictionarygetoradd)
20 | - [Constructors](#constructors)
21 | - [WindowsIdentity.RunImpersonated](#windowsidentityrunimpersonated)
22 |
23 | # Asynchronous Programming
24 |
25 | Asynchronous programming has been around for several years on the .NET platform but has historically been very difficult to do well. Since the introduction of async/await
26 | in C# 5 asynchronous programming has become mainstream. Modern frameworks (like ASP.NET Core) are fully asynchronous and it's very hard to avoid the async keyword when writing
27 | web services. As a result, there's been lots of confusion on the best practices for async and how to use it properly. This section will try to lay out some guidance with examples of bad and good patterns of how to write asynchronous code.
28 |
29 | ## Asynchrony is viral
30 |
31 | Once you go async, all of your callers **SHOULD** be async, since efforts to be async amount to nothing unless the entire callstack is async. In many cases, being partially async can be worse than being entirely synchronous. Therefore it is best to go all in, and make everything async at once.
32 |
33 | ❌ **BAD** This example uses the `Task.Result` and as a result blocks the current thread to wait for the result. This is an example of [sync over async](#avoid-using-taskresult-and-taskwait).
34 |
35 | ```C#
36 | public int DoSomethingAsync()
37 | {
38 | var result = CallDependencyAsync().Result;
39 | return result + 1;
40 | }
41 | ```
42 |
43 | :white_check_mark: **GOOD** This example uses the await keyword to get the result from `CallDependencyAsync`.
44 |
45 | ```C#
46 | public async Task DoSomethingAsync()
47 | {
48 | var result = await CallDependencyAsync();
49 | return result + 1;
50 | }
51 | ```
52 |
53 | ## Async void
54 |
55 | Use of async void in ASP.NET Core applications is **ALWAYS** bad. Avoid it, never do it. Typically, it's used when developers are trying to implement fire and forget patterns triggered by a controller action. Async void methods will crash the process if an exception is thrown. We'll look at more of the patterns that cause developers to do this in ASP.NET Core applications but here's a simple example:
56 |
57 | ❌ **BAD** Async void methods can't be tracked and therefore unhandled exceptions can result in application crashes.
58 |
59 | ```C#
60 | public class MyController : Controller
61 | {
62 | [HttpPost("/start")]
63 | public IActionResult Post()
64 | {
65 | BackgroundOperationAsync();
66 | return Accepted();
67 | }
68 |
69 | public async void BackgroundOperationAsync()
70 | {
71 | var result = await CallDependencyAsync();
72 | DoSomething(result);
73 | }
74 | }
75 | ```
76 |
77 | :white_check_mark: **GOOD** `Task`-returning methods are better since unhandled exceptions trigger the [`TaskScheduler.UnobservedTaskException`](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler.unobservedtaskexception?view=netframework-4.7.2).
78 |
79 | ```C#
80 | public class MyController : Controller
81 | {
82 | [HttpPost("/start")]
83 | public IActionResult Post()
84 | {
85 | Task.Run(BackgroundOperationAsync);
86 | return Accepted();
87 | }
88 |
89 | public async Task BackgroundOperationAsync()
90 | {
91 | var result = await CallDependencyAsync();
92 | DoSomething(result);
93 | }
94 | }
95 | ```
96 |
97 | ## Prefer `Task.FromResult` over `Task.Run` for pre-computed or trivially computed data
98 |
99 | For pre-computed results, there's no need to call `Task.Run`, that will end up queuing a work item to the thread pool that will immediately complete with the pre-computed value. Instead, use `Task.FromResult`, to create a task wrapping already computed data.
100 |
101 | ❌ **BAD** This example wastes a thread-pool thread to return a trivially computed value.
102 |
103 | ```C#
104 | public class MyLibrary
105 | {
106 | public Task AddAsync(int a, int b)
107 | {
108 | return Task.Run(() => a + b);
109 | }
110 | }
111 | ```
112 |
113 | :white_check_mark: **GOOD** This example uses `Task.FromResult` to return the trivially computed value. It does not use any extra threads as a result.
114 |
115 | ```C#
116 | public class MyLibrary
117 | {
118 | public Task AddAsync(int a, int b)
119 | {
120 | return Task.FromResult(a + b);
121 | }
122 | }
123 | ```
124 |
125 | :bulb:**NOTE: Using `Task.FromResult` will result in a `Task` allocation. Using `ValueTask` can completely remove that allocation.**
126 |
127 | :white_check_mark: **GOOD** This example uses a `ValueTask` to return the trivially computed value. It does not use any extra threads as a result. It also does not allocate an object on the managed heap.
128 |
129 | ```C#
130 | public class MyLibrary
131 | {
132 | public ValueTask AddAsync(int a, int b)
133 | {
134 | return new ValueTask(a + b);
135 | }
136 | }
137 | ```
138 |
139 | ## Avoid using Task.Run for long running work that blocks the thread
140 |
141 | Long running work in this context refers to a thread that's running for the lifetime of the application doing background work (like processing queue items, or sleeping and waking up to process some data). `Task.Run` will queue a work item to the thread pool. The assumption is that that work will finish quickly (or quickly enough to allow reusing that thread within some reasonable timeframe). Stealing a thread-pool thread for long-running work is bad since it takes that thread away from other work that could be done (timer callbacks, task continuations etc). Instead, spawn a new thread manually to do long running blocking work.
142 |
143 | :bulb: **NOTE: The thread pool grows if you block threads but it's bad practice to do so.**
144 |
145 | :bulb: **NOTE:`Task.Factory.StartNew` has an option `TaskCreationOptions.LongRunning` that under the covers creates a new thread and returns a Task that represents the execution. Using this properly requires several non-obvious parameters to be passed in to get the right behavior on all platforms.**
146 |
147 | :bulb: **NOTE: Don't use `TaskCreationOptions.LongRunning` with async code as this will create a new thread which will be destroyed after first `await`.**
148 |
149 |
150 | ❌ **BAD** This example steals a thread-pool thread forever, to execute queued work on a `BlockingCollection`.
151 |
152 | ```C#
153 | public class QueueProcessor
154 | {
155 | private readonly BlockingCollection _messageQueue = new BlockingCollection();
156 |
157 | public void StartProcessing()
158 | {
159 | Task.Run(ProcessQueue);
160 | }
161 |
162 | public void Enqueue(Message message)
163 | {
164 | _messageQueue.Add(message);
165 | }
166 |
167 | private void ProcessQueue()
168 | {
169 | foreach (var item in _messageQueue.GetConsumingEnumerable())
170 | {
171 | ProcessItem(item);
172 | }
173 | }
174 |
175 | private void ProcessItem(Message message) { }
176 | }
177 | ```
178 |
179 | :white_check_mark: **GOOD** This example uses a dedicated thread to process the message queue instead of a thread-pool thread.
180 |
181 | ```C#
182 | public class QueueProcessor
183 | {
184 | private readonly BlockingCollection _messageQueue = new BlockingCollection();
185 |
186 | public void StartProcessing()
187 | {
188 | var thread = new Thread(ProcessQueue)
189 | {
190 | // This is important as it allows the process to exit while this thread is running
191 | IsBackground = true
192 | };
193 | thread.Start();
194 | }
195 |
196 | public void Enqueue(Message message)
197 | {
198 | _messageQueue.Add(message);
199 | }
200 |
201 | private void ProcessQueue()
202 | {
203 | foreach (var item in _messageQueue.GetConsumingEnumerable())
204 | {
205 | ProcessItem(item);
206 | }
207 | }
208 |
209 | private void ProcessItem(Message message) { }
210 | }
211 | ```
212 |
213 | ## Avoid using `Task.Result` and `Task.Wait`
214 |
215 | There are very few ways to use `Task.Result` and `Task.Wait` correctly so the general advice is to completely avoid using them in your code.
216 |
217 | ### :warning: Sync over `async`
218 |
219 | Using `Task.Result` or `Task.Wait` to block wait on an asynchronous operation to complete is *MUCH* worse than calling a truly synchronous API to block. This phenomenon is dubbed "Sync over async". Here is what happens at a very high level:
220 |
221 | - An asynchronous operation is kicked off.
222 | - The calling thread is blocked waiting for that operation to complete.
223 | - When the asynchronous operation completes, it unblocks the code waiting on that operation. This takes place on another thread.
224 |
225 | The result is that we need to use 2 threads instead of 1 to complete synchronous operations. This usually leads to [thread-pool starvation](https://blogs.msdn.microsoft.com/vancem/2018/10/16/diagnosing-net-core-threadpool-starvation-with-perfview-why-my-service-is-not-saturating-all-cores-or-seems-to-stall/) and results in service outages.
226 |
227 | ### :warning: Deadlocks
228 |
229 | The `SynchronizationContext` is an abstraction that gives application models a chance to control where asynchronous continuations run. ASP.NET (non-core), WPF and Windows Forms each have an implementation that will result in a deadlock if Task.Wait or Task.Result is used on the main thread. This behavior has led to a bunch of "clever" code snippets that show the "right" way to block waiting for a Task. The truth is, there's no good way to block waiting for a Task to complete.
230 |
231 | :bulb:**NOTE: ASP.NET Core does not have a `SynchronizationContext` and is not prone to the deadlock problem.**
232 |
233 | ❌ **BAD** The below are all examples that are, in one way or another, trying to avoid the deadlock situation but still succumb to "sync over async" problems.
234 |
235 | ```C#
236 | public string DoOperationBlocking()
237 | {
238 | // Bad - Blocking the thread that enters.
239 | // DoAsyncOperation will be scheduled on the default task scheduler, and remove the risk of deadlocking.
240 | // In the case of an exception, this method will throw an AggregateException wrapping the original exception.
241 | return Task.Run(() => DoAsyncOperation()).Result;
242 | }
243 |
244 | public string DoOperationBlocking2()
245 | {
246 | // Bad - Blocking the thread that enters.
247 | // DoAsyncOperation will be scheduled on the default task scheduler, and remove the risk of deadlocking.
248 | return Task.Run(() => DoAsyncOperation()).GetAwaiter().GetResult();
249 | }
250 |
251 | public string DoOperationBlocking3()
252 | {
253 | // Bad - Blocking the thread that enters, and blocking the theadpool thread inside.
254 | // In the case of an exception, this method will throw an AggregateException containing another AggregateException, containing the original exception.
255 | return Task.Run(() => DoAsyncOperation().Result).Result;
256 | }
257 |
258 | public string DoOperationBlocking4()
259 | {
260 | // Bad - Blocking the thread that enters, and blocking the theadpool thread inside.
261 | return Task.Run(() => DoAsyncOperation().GetAwaiter().GetResult()).GetAwaiter().GetResult();
262 | }
263 |
264 | public string DoOperationBlocking5()
265 | {
266 | // Bad - Blocking the thread that enters.
267 | // Bad - No effort has been made to prevent a present SynchonizationContext from becoming deadlocked.
268 | // In the case of an exception, this method will throw an AggregateException wrapping the original exception.
269 | return DoAsyncOperation().Result;
270 | }
271 |
272 | public string DoOperationBlocking6()
273 | {
274 | // Bad - Blocking the thread that enters.
275 | // Bad - No effort has been made to prevent a present SynchonizationContext from becoming deadlocked.
276 | return DoAsyncOperation().GetAwaiter().GetResult();
277 | }
278 |
279 | public string DoOperationBlocking7()
280 | {
281 | // Bad - Blocking the thread that enters.
282 | // Bad - No effort has been made to prevent a present SynchonizationContext from becoming deadlocked.
283 | var task = DoAsyncOperation();
284 | task.Wait();
285 | return task.GetAwaiter().GetResult();
286 | }
287 | ```
288 |
289 | ## Prefer `await` over `ContinueWith`
290 |
291 | `Task` existed before the async/await keywords were introduced and as such provided ways to execute continuations without relying on the language. Although these methods are still valid to use, we generally recommend that you prefer `async`/`await` to using `ContinueWith`. `ContinueWith` also does not capture the `SynchronizationContext` and as a result is actually semantically different to `async`/`await`.
292 |
293 | ❌ **BAD** The example uses `ContinueWith` instead of `async`
294 |
295 | ```C#
296 | public Task DoSomethingAsync()
297 | {
298 | return CallDependencyAsync().ContinueWith(task =>
299 | {
300 | return task.Result + 1;
301 | });
302 | }
303 | ```
304 |
305 | :white_check_mark: **GOOD** This example uses the `await` keyword to get the result from `CallDependencyAsync`.
306 |
307 | ```C#
308 | public async Task DoSomethingAsync()
309 | {
310 | var result = await CallDependencyAsync();
311 | return result + 1;
312 | }
313 | ```
314 |
315 | ## Always create `TaskCompletionSource` with `TaskCreationOptions.RunContinuationsAsynchronously`
316 |
317 | `TaskCompletionSource` is an important building block for libraries trying to adapt things that are not inherently awaitable to be awaitable via a `Task`. It is also commonly used to build higher-level operations (such as batching and other combinators) on top of existing asynchronous APIs. By default, `Task` continuations will run *inline* on the same thread that calls Try/Set(Result/Exception/Canceled). As a library author, this means having to understand that calling code can resume directly on your thread. This is extremely dangerous and can result in deadlocks, thread-pool starvation, corruption of state (if code runs unexpectedly) and more.
318 |
319 | Always use `TaskCreationOptions.RunContinuationsAsynchronously` when creating the `TaskCompletionSource`. This will dispatch the continuation onto the thread pool instead of executing it inline.
320 |
321 | ❌ **BAD** This example does not use `TaskCreationOptions.RunContinuationsAsynchronously` when creating the `TaskCompletionSource`.
322 |
323 | ```C#
324 | public Task DoSomethingAsync()
325 | {
326 | var tcs = new TaskCompletionSource();
327 |
328 | var operation = new LegacyAsyncOperation();
329 | operation.Completed += result =>
330 | {
331 | // Code awaiting on this task will resume on this thread!
332 | tcs.SetResult(result);
333 | };
334 |
335 | return tcs.Task;
336 | }
337 | ```
338 |
339 | :white_check_mark: **GOOD** This example uses `TaskCreationOptions.RunContinuationsAsynchronously` when creating the `TaskCompletionSource`.
340 |
341 | ```C#
342 | public Task DoSomethingAsync()
343 | {
344 | var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
345 |
346 | var operation = new LegacyAsyncOperation();
347 | operation.Completed += result =>
348 | {
349 | // Code awaiting on this task will resume on a different thread-pool thread
350 | tcs.SetResult(result);
351 | };
352 |
353 | return tcs.Task;
354 | }
355 | ```
356 |
357 | :bulb:**NOTE: There are 2 enums that look alike. [`TaskCreationOptions.RunContinuationsAsynchronously`](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=netcore-2.0#System_Threading_Tasks_TaskCreationOptions_RunContinuationsAsynchronously) and [`TaskContinuationOptions.RunContinuationsAsynchronously`](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcontinuationoptions?view=netcore-2.0). Be careful not to confuse their usage.**
358 |
359 | ## Always dispose `CancellationTokenSource`(s) used for timeouts
360 |
361 | `CancellationTokenSource` objects that are used for timeouts (are created with timers or uses the `CancelAfter` method), can put pressure on the timer queue if not disposed.
362 |
363 | ❌ **BAD** This example does not dispose the `CancellationTokenSource` and as a result the timer stays in the queue for 10 seconds after each request is made.
364 |
365 | ```C#
366 | public async Task HttpClientAsyncWithCancellationBad()
367 | {
368 | var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
369 |
370 | using (var client = _httpClientFactory.CreateClient())
371 | {
372 | var response = await client.GetAsync("http://backend/api/1", cts.Token);
373 | return await response.Content.ReadAsStreamAsync();
374 | }
375 | }
376 | ```
377 |
378 | :white_check_mark: **GOOD** This example disposes the `CancellationTokenSource` and properly removes the timer from the queue.
379 |
380 | ```C#
381 | public async Task HttpClientAsyncWithCancellationGood()
382 | {
383 | using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
384 | {
385 | using (var client = _httpClientFactory.CreateClient())
386 | {
387 | var response = await client.GetAsync("http://backend/api/1", cts.Token);
388 | return await response.Content.ReadAsStreamAsync();
389 | }
390 | }
391 | }
392 | ```
393 |
394 | ## Always flow `CancellationToken`(s) to APIs that take a `CancellationToken`
395 |
396 | Cancellation is cooperative in .NET. Everything in the call-chain has to be explicitly passed the `CancellationToken` in order for it to work well. This means you need to explicitly pass the token into other APIs that take a token if you want cancellation to be most effective.
397 |
398 | ❌ **BAD** This example neglects to pass the `CancellationToken` to `Stream.ReadAsync` making the operation effectively not cancellable.
399 |
400 | ```C#
401 | public async Task DoAsyncThing(CancellationToken cancellationToken = default)
402 | {
403 | byte[] buffer = new byte[1024];
404 | // We forgot to pass flow cancellationToken to ReadAsync
405 | int read = await _stream.ReadAsync(buffer, 0, buffer.Length);
406 | return Encoding.UTF8.GetString(buffer, 0, read);
407 | }
408 | ```
409 |
410 | :white_check_mark: **GOOD** This example passes the `CancellationToken` into `Stream.ReadAsync`.
411 |
412 | ```C#
413 | public async Task DoAsyncThing(CancellationToken cancellationToken = default)
414 | {
415 | byte[] buffer = new byte[1024];
416 | // This properly flows cancellationToken to ReadAsync
417 | int read = await _stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
418 | return Encoding.UTF8.GetString(buffer, 0, read);
419 | }
420 | ```
421 |
422 | ## Cancelling uncancellable operations
423 |
424 | One of the coding patterns that appears when doing asynchronous programming is cancelling an uncancellable operation. This usually means creating another task that completes when a timeout or `CancellationToken` fires, and then using `Task.WhenAny` to detect a complete or cancelled operation.
425 |
426 | ### Using CancellationTokens
427 |
428 | ❌ **BAD** This example uses `Task.Delay(-1, token)` to create a `Task` that completes when the `CancellationToken` fires, but if it doesn't fire, there's no way to dispose the `CancellationTokenRegistration`. This can lead to a memory leak.
429 |
430 | ```C#
431 | public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
432 | {
433 | // There's no way to dispose the registration
434 | var delayTask = Task.Delay(-1, cancellationToken);
435 |
436 | var resultTask = await Task.WhenAny(task, delayTask);
437 | if (resultTask == delayTask)
438 | {
439 | // Operation cancelled
440 | throw new OperationCanceledException();
441 | }
442 |
443 | return await task;
444 | }
445 | ```
446 |
447 | :white_check_mark: **GOOD** This example disposes the `CancellationTokenRegistration` when one of the `Task(s)` complete.
448 |
449 | ```C#
450 | public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
451 | {
452 | var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
453 |
454 | // This disposes the registration as soon as one of the tasks trigger
455 | using (cancellationToken.Register(state =>
456 | {
457 | ((TaskCompletionSource)state).TrySetResult(null);
458 | },
459 | tcs))
460 | {
461 | var resultTask = await Task.WhenAny(task, tcs.Task);
462 | if (resultTask == tcs.Task)
463 | {
464 | // Operation cancelled
465 | throw new OperationCanceledException(cancellationToken);
466 | }
467 |
468 | return await task;
469 | }
470 | }
471 | ```
472 |
473 | ### Using a timeout
474 |
475 | ❌ **BAD** This example does not cancel the timer even if the operation successfuly completes. This means you could end up with lots of timers, which can flood the timer queue.
476 |
477 | ```C#
478 | public static async Task TimeoutAfter(this Task task, TimeSpan timeout)
479 | {
480 | var delayTask = Task.Delay(timeout);
481 |
482 | var resultTask = await Task.WhenAny(task, delayTask);
483 | if (resultTask == delayTask)
484 | {
485 | // Operation cancelled
486 | throw new OperationCanceledException();
487 | }
488 |
489 | return await task;
490 | }
491 | ```
492 |
493 | :white_check_mark: **GOOD** This example cancels the timer if the operation succesfully completes.
494 |
495 | ```C#
496 | public static async Task TimeoutAfter(this Task task, TimeSpan timeout)
497 | {
498 | using (var cts = new CancellationTokenSource())
499 | {
500 | var delayTask = Task.Delay(timeout, cts.Token);
501 |
502 | var resultTask = await Task.WhenAny(task, delayTask);
503 | if (resultTask == delayTask)
504 | {
505 | // Operation cancelled
506 | throw new OperationCanceledException();
507 | }
508 | else
509 | {
510 | // Cancel the timer task so that it does not fire
511 | cts.Cancel();
512 | }
513 |
514 | return await task;
515 | }
516 | }
517 | ```
518 |
519 | ## Always call `FlushAsync` on `StreamWriter`(s) or `Stream`(s) before calling `Dispose`
520 |
521 | When writing to a `Stream` or `StreamWriter`, even if the asynchronous overloads are used for writing, the underlying data might be buffered. When data is buffered, disposing the `Stream` or `StreamWriter` via the `Dispose` method will synchronously write/flush, which results in blocking the thread and could lead to thread-pool starvation. Either use the asynchronous `DisposeAsync` method (for example via `await using`) or call `FlushAsync` before calling `Dispose`.
522 |
523 | :bulb:**NOTE: This is only problematic if the underlying subsystem does IO.**
524 |
525 | ❌ **BAD** This example ends up blocking the request by writing synchronously to the HTTP-response body.
526 |
527 | ```C#
528 | app.Run(async context =>
529 | {
530 | // The implicit Dispose call will synchronously write to the response body
531 | using (var streamWriter = new StreamWriter(context.Response.Body))
532 | {
533 | await streamWriter.WriteAsync("Hello World");
534 | }
535 | });
536 | ```
537 |
538 | :white_check_mark: **GOOD** This example asynchronously flushes any buffered data while disposing the `StreamWriter`.
539 |
540 | ```C#
541 | app.Run(async context =>
542 | {
543 | // The implicit AsyncDispose call will flush asynchronously
544 | await using (var streamWriter = new StreamWriter(context.Response.Body))
545 | {
546 | await streamWriter.WriteAsync("Hello World");
547 | }
548 | });
549 | ```
550 |
551 | :white_check_mark: **GOOD** This example asynchronously flushes any buffered data before disposing the `StreamWriter`.
552 |
553 | ```C#
554 | app.Run(async context =>
555 | {
556 | using (var streamWriter = new StreamWriter(context.Response.Body))
557 | {
558 | await streamWriter.WriteAsync("Hello World");
559 |
560 | // Force an asynchronous flush
561 | await streamWriter.FlushAsync();
562 | }
563 | });
564 | ```
565 |
566 | ## Prefer `async`/`await` over directly returning `Task`
567 |
568 | There are benefits to using the `async`/`await` keyword instead of directly returning the `Task`:
569 | - Asynchronous and synchronous exceptions are normalized to always be asynchronous.
570 | - The code is easier to modify (consider adding a `using`, for example).
571 | - Diagnostics of asynchronous methods are easier (debugging hangs etc).
572 | - Exceptions thrown will be automatically wrapped in the returned `Task` instead of surprising the caller with an actual exception.
573 |
574 | ❌ **BAD** This example directly returns the `Task` to the caller.
575 |
576 | ```C#
577 | public Task DoSomethingAsync()
578 | {
579 | return CallDependencyAsync();
580 | }
581 | ```
582 |
583 | :white_check_mark: **GOOD** This examples uses async/await instead of directly returning the Task.
584 |
585 | ```C#
586 | public async Task DoSomethingAsync()
587 | {
588 | return await CallDependencyAsync();
589 | }
590 | ```
591 |
592 | :bulb:**NOTE: There are performance considerations when using an async state machine over directly returning the `Task`. It's always faster to directly return the `Task` since it does less work but you end up changing the behavior and potentially losing some of the benefits of the async state machine.**
593 |
594 | ## ConfigureAwait
595 |
596 | TBD
597 |
598 | # Scenarios
599 |
600 | The above tries to distill general guidance, but doesn't do justice to the kinds of real-world situations that cause code like this to be written in the first place (bad code). This section tries to take concrete examples from real applications and turn them into something simple to help you relate these problems to existing codebases.
601 |
602 | ## `Timer` callbacks
603 |
604 | ❌ **BAD** The `Timer` callback is `void`-returning and we have asynchronous work to execute. This example uses `async void` to accomplish it and as a result can crash the process if an exception occurs.
605 |
606 | ```C#
607 | public class Pinger
608 | {
609 | private readonly Timer _timer;
610 | private readonly HttpClient _client;
611 |
612 | public Pinger(HttpClient client)
613 | {
614 | _client = client;
615 | _timer = new Timer(Heartbeat, null, 1000, 1000);
616 | }
617 |
618 | public async void Heartbeat(object state)
619 | {
620 | await _client.GetAsync("http://mybackend/api/ping");
621 | }
622 | }
623 | ```
624 |
625 | ❌ **BAD** This attempts to block in the `Timer` callback. This may result in thread-pool starvation and is an example of [sync over async](#warning-sync-over-async)
626 |
627 | ```C#
628 | public class Pinger
629 | {
630 | private readonly Timer _timer;
631 | private readonly HttpClient _client;
632 |
633 | public Pinger(HttpClient client)
634 | {
635 | _client = client;
636 | _timer = new Timer(Heartbeat, null, 1000, 1000);
637 | }
638 |
639 | public void Heartbeat(object state)
640 | {
641 | _client.GetAsync("http://mybackend/api/ping").GetAwaiter().GetResult();
642 | }
643 | }
644 | ```
645 |
646 | :white_check_mark: **GOOD** This example uses an `async Task`-based method and discards the `Task` in the `Timer` callback. If this method fails, it will not crash the process. Instead, it will fire the [`TaskScheduler.UnobservedTaskException`](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler.unobservedtaskexception?view=netframework-4.7.2) event.
647 |
648 | ```C#
649 | public class Pinger
650 | {
651 | private readonly Timer _timer;
652 | private readonly HttpClient _client;
653 |
654 | public Pinger(HttpClient client)
655 | {
656 | _client = client;
657 | _timer = new Timer(Heartbeat, null, 1000, 1000);
658 | }
659 |
660 | public void Heartbeat(object state)
661 | {
662 | // Discard the result
663 | _ = DoAsyncPing();
664 | }
665 |
666 | private async Task DoAsyncPing()
667 | {
668 | await _client.GetAsync("http://mybackend/api/ping");
669 | }
670 | }
671 | ```
672 |
673 | ## Implicit `async void` delegates
674 |
675 | Imagine a `BackgroundQueue` with a `FireAndForget` that takes a callback. This method will execute the callback at some time in the future.
676 |
677 | ❌ **BAD** This will force callers to either block in the callback or use an `async void` delegate.
678 |
679 | ```C#
680 | public class BackgroundQueue
681 | {
682 | public static void FireAndForget(Action action) { }
683 | }
684 | ```
685 |
686 | ❌ **BAD** This calling code is creating an `async void` method implicitly. The compiler fully supports this today.
687 |
688 | ```C#
689 | public class Program
690 | {
691 | public void Main(string[] args)
692 | {
693 | var httpClient = new HttpClient();
694 | BackgroundQueue.FireAndForget(async () =>
695 | {
696 | await httpClient.GetAsync("http://pinger/api/1");
697 | });
698 |
699 | Console.ReadLine();
700 | }
701 | }
702 | ```
703 |
704 | :white_check_mark: **GOOD** This BackgroundQueue implementation offers both sync and `async` callback overloads.
705 |
706 | ```C#
707 | public class BackgroundQueue
708 | {
709 | public static void FireAndForget(Action action) { }
710 | public static void FireAndForget(Func action) { }
711 | }
712 | ```
713 |
714 | ## `ConcurrentDictionary.GetOrAdd`
715 |
716 | It's pretty common to cache the result of an asynchronous operation and `ConcurrentDictionary` is a good data structure for doing that. `GetOrAdd` is a convenience API for trying to get an item if it's already there or adding it if it isn't. The callback is synchronous so it's tempting to write code that uses `Task.Result` to produce the value of an asynchronous process but that can lead to thread-pool starvation.
717 |
718 | ❌ **BAD** This may result in thread-pool starvation since we're blocking the request thread if the person data is not cached.
719 |
720 | ```C#
721 | public class PersonController : Controller
722 | {
723 | private AppDbContext _db;
724 |
725 | // This cache needs expiration
726 | private static ConcurrentDictionary _cache = new ConcurrentDictionary();
727 |
728 | public PersonController(AppDbContext db)
729 | {
730 | _db = db;
731 | }
732 |
733 | public IActionResult Get(int id)
734 | {
735 | var person = _cache.GetOrAdd(id, (key) => _db.People.FindAsync(key).Result);
736 | return Ok(person);
737 | }
738 | }
739 | ```
740 |
741 | :white_check_mark: **GOOD** This implementation won't result in thread-pool starvation since we're storing a task instead of the result itself.
742 |
743 | :warning: `ConcurrentDictionary.GetOrAdd` will potentially run the cache callback multiple times in parallel. This can result in kicking off expensive computations multiple times.
744 |
745 | ```C#
746 | public class PersonController : Controller
747 | {
748 | private AppDbContext _db;
749 |
750 | // This cache needs expiration
751 | private static ConcurrentDictionary> _cache = new ConcurrentDictionary>();
752 |
753 | public PersonController(AppDbContext db)
754 | {
755 | _db = db;
756 | }
757 |
758 | public async Task Get(int id)
759 | {
760 | var person = await _cache.GetOrAdd(id, (key) => _db.People.FindAsync(key));
761 | return Ok(person);
762 | }
763 | }
764 | ```
765 |
766 | :white_check_mark: **GOOD** This implementation fixes the multiple-executing callback issue by using the `async` lazy pattern.
767 |
768 | ```C#
769 | public class PersonController : Controller
770 | {
771 | private AppDbContext _db;
772 |
773 | // This cache needs expiration
774 | private static ConcurrentDictionary> _cache = new ConcurrentDictionary>();
775 |
776 | public PersonController(AppDbContext db)
777 | {
778 | _db = db;
779 | }
780 |
781 | public async Task Get(int id)
782 | {
783 | var person = await _cache.GetOrAdd(id, (key) => new AsyncLazy(() => _db.People.FindAsync(key))).Value;
784 | return Ok(person);
785 | }
786 |
787 | private class AsyncLazy : Lazy>
788 | {
789 | public AsyncLazy(Func> valueFactory) : base(valueFactory)
790 | {
791 | }
792 | }
793 | }
794 | ```
795 |
796 | ## Constructors
797 |
798 | Constructors are synchronous. If you need to initialize some logic that may be asynchronous, there are a couple of patterns for dealing with this.
799 |
800 | Here's an example of using a client API that needs to connect asynchronously before use.
801 |
802 | ```C#
803 | public interface IRemoteConnectionFactory
804 | {
805 | Task ConnectAsync();
806 | }
807 |
808 | public interface IRemoteConnection
809 | {
810 | Task PublishAsync(string channel, string message);
811 | Task DisposeAsync();
812 | }
813 | ```
814 |
815 |
816 | ❌ **BAD** This example uses `Task.Result` to get the connection in the constructor. This could lead to thread-pool starvation and deadlocks.
817 |
818 | ```C#
819 | public class Service : IService
820 | {
821 | private readonly IRemoteConnection _connection;
822 |
823 | public Service(IRemoteConnectionFactory connectionFactory)
824 | {
825 | _connection = connectionFactory.ConnectAsync().Result;
826 | }
827 | }
828 | ```
829 |
830 | :white_check_mark: **GOOD** This implementation uses a static factory pattern in order to allow asynchronous construction:
831 |
832 | ```C#
833 | public class Service : IService
834 | {
835 | private readonly IRemoteConnection _connection;
836 |
837 | private Service(IRemoteConnection connection)
838 | {
839 | _connection = connection;
840 | }
841 |
842 | public static async Task CreateAsync(IRemoteConnectionFactory connectionFactory)
843 | {
844 | return new Service(await connectionFactory.ConnectAsync());
845 | }
846 | }
847 | ```
848 |
849 | ## WindowsIdentity.RunImpersonated
850 |
851 | This API runs the specified action as the impersonated Windows identity. Unfortunately there's no asynchronous version of the callback.
852 |
853 | ❌ **BAD** This example tries to execute the query asynchronously, and then wait for it outside of the call to `RunImpersonated`. This will throw because the query might be executing outside of the impersonation context.
854 |
855 | ```C#
856 | public async Task> GetDataImpersonatedAsync(SafeAccessTokenHandle safeAccessTokenHandle)
857 | {
858 | Task> products = null;
859 | WindowsIdentity.RunImpersonated(
860 | safeAccessTokenHandle,
861 | context =>
862 | {
863 | products = _db.QueryAsync("SELECT Name from Products");
864 | }};
865 | return await products;
866 | }
867 | ```
868 |
869 | ❌ **BAD** This example uses `Task.Result` to get the connection in the constructor. This could lead to thread-pool starvation and deadlocks.
870 |
871 | ```C#
872 | public IEnumerable GetDataImpersonatedAsync(SafeAccessTokenHandle safeAccessTokenHandle)
873 | {
874 | return WindowsIdentity.RunImpersonated(
875 | safeAccessTokenHandle,
876 | context => _db.QueryAsync("SELECT Name from Products").Result);
877 | }
878 | ```
879 |
880 | :white_check_mark: **GOOD** This example awaits the result of `RunImpersonated` (the delegate is `Func>>` in this case).
881 |
882 | ```C#
883 | public async Task> GetDataImpersonatedAsync(SafeAccessTokenHandle safeAccessTokenHandle)
884 | {
885 | return await WindowsIdentity.RunImpersonated(
886 | safeAccessTokenHandle,
887 | context => _db.QueryAsync("SELECT Name from Products"));
888 | }
889 | ```
890 |
--------------------------------------------------------------------------------