ASP.NET Core Diagnostic Scenarios

repository·master·Indexed 27 days ago

https://github.com/davidfowl/aspnetcorediagnosticscenarios

A collection of real-world diagnostic scenarios and problematic application patterns for ASP.NET Core. This repository provides walkthroughs and guidance on solving common issues and writing scalable web services, with specific focus on general ASP.NET Core best practices and asynchronous programming.

Tokens
10K
Snippets
29
Records
34
Agent score
94%

What's inside aspnetcorediagnosticscenarios

  1. Explore ASP.NET Core Diagnostic Scenarios and Scalability Guides

    master

    This repository provides a collection of problematic application patterns encountered in real-life ASP.NET Core applications and walkthroughs on how to resolve them. It is designed to help developers avoid common pitfalls when writing scalable services.

    Key guidance areas include:

    • General ASP.NET Core guidance: Found in AspNetCoreGuidance.md.
    • Asynchronous Programming guidance: Found in AsyncGuidance.md.
  2. Prefer await over ContinueWith

    master

    When performing continuations on a Task, prefer the async/await keywords over the ContinueWith method. ContinueWith does not capture the SynchronizationContext, making it semantically different from async/await, and it is generally less readable and harder to manage.

    // GOOD: This example uses the await keyword to get the result from CallDependencyAsync.
    public async Task<int> DoSomethingAsync()
    {
        var result = await CallDependencyAsync();
        return result + 1;
    }
  3. Avoid using Task.Result and Task.Wait

    master

    Avoid using .Result or .Wait() to block on asynchronous operations. This pattern, known as "Sync over async", is highly inefficient because it requires two threads to complete a single operation (one thread blocked waiting, and another thread performing the work), which can lead to thread-pool starvation and service outages.

    While ASP.NET Core does not have a SynchronizationContext and is therefore not prone to the deadlocks common in WPF or ASP.NET (non-core), using these blocking calls still causes performance degradation and resource exhaustion.

  4. Use WindowsIdentity.RunImpersonatedAsync for impersonation

    master

    When performing database queries or other work under a Windows identity, ensure the work is completed within the impersonation context.

    • In .NET 5.0 or newer, use WindowsIdentity.RunImpersonatedAsync and await its result.
    • In older versions, use WindowsIdentity.RunImpersonated and ensure the delegate returns a Task that is awaited within the call.
    // Recommended for .NET 5.0+
    public async Task<IEnumerable<Product>> GetDataImpersonatedAsync(SafeAccessTokenHandle safeAccessTokenHandle)
    {
        return await WindowsIdentity.RunImpersonatedAsync(
            safeAccessTokenHandle, 
            context => _db.QueryAsync("SELECT Name from Products"));
    }
  5. Prefer using HttpRequest.ReadFormAsync() over HttpRequest.Form

    master

    To avoid 'sync over async' issues and potential thread pool starvation, always prefer HttpRequest.ReadFormAsync() over the HttpRequest.Form property. The only exception is if HttpRequest.ReadFormAsync() has already been called and the cached form value is being accessed via HttpRequest.Form.

    public class MyController : Controller
    {
        [HttpPost("/form-body")]
        public async Task<IActionResult> Post()
        {
            var form = await HttpRequest.ReadFormAsync();
            
            Process(form["id"], form["name"]);
    
            return Accepted();
        }
    }
  6. Handle long-running background work correctly

    master

    Avoid using Task.Run for work that runs for the lifetime of the application (e.g., processing a queue). This 'steals' a thread-pool thread that should be used for short-lived tasks. Instead, use a dedicated Thread with IsBackground = true or use Task.Factory.StartNew with TaskCreationOptions.LongRunning.

    // ✅ GOOD: Using a dedicated background thread
    public void StartProcessing()
    {
        var thread = new Thread(ProcessQueue) 
        {
            IsBackground = true
        };
        thread.Start();
    }
    
    // ✅ GOOD: Using TaskCreationOptions.LongRunning
    public Task StartProcessing() => 
        Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
  7. Handle Timer callbacks safely

    master

    When using System.Threading.Timer, avoid using async void for the callback, as exceptions can crash the process. Also, avoid blocking the callback with .Result or .Wait(), which causes thread-pool starvation.

    For .NET 6 and later, prefer using PeriodicTimer in a dedicated background loop. This allows you to await the timer ticks and handle exceptions gracefully within the loop.

    // Recommended for .NET 6+
    public class Pinger : IDisposable
    {
        private readonly PeriodicTimer _timer;
        private readonly HttpClient _client;
    
        public Pinger(HttpClient client)
        {
            _client = client;
            _timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
            _ = Task.Run(DoAsyncPings);
        }
    
        public void Dispose() => _timer.Dispose();
    
        private async Task DoAsyncPings()
        {
            while (await _timer.WaitForNextTickAsync())
            {
                // TODO: Handle exceptions
                await _client.GetAsync("http://mybackend/api/ping");
            }
        }
    }
  8. Use TaskCreationOptions.RunContinuationsAsynchronously with TaskCompletionSource<T>

    master

    When building libraries that adapt non-awaitable APIs to be awaitable using TaskCompletionSource<T>, always initialize the source with TaskCreationOptions.RunContinuationsAsynchronously.

    By default, continuations run inline on the thread that calls TrySetResult (or similar). This can cause the calling code to resume directly on your library's internal thread, leading to deadlocks, thread-pool starvation, or state corruption. Using this flag ensures the continuation is dispatched to the thread pool instead.

    // GOOD: This example uses TaskCreationOptions.RunContinuationsAsynchronously when creating the TaskCompletionSource<T>.
    public Task<int> DoSomethingAsync()
    {
        var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
        
        var operation = new LegacyAsyncOperation();
        operation.Completed += result =>
        {
            // Code awaiting on this task will resume on a different thread-pool thread
            tcs.SetResult(result);
        };
        
        return tcs.Task;
    }
  9. Do not access HttpContext from multiple threads in parallel

    master

    HttpContext is not thread-safe. Accessing it from multiple threads in parallel can cause corruption, hangs, crashes, or data corruption. If you need to use data from the HttpContext (like Request.Path) inside parallel tasks or background operations, copy the required data into local variables before starting the parallel work.

    public class AsyncController : Controller
    {
        [HttpGet("/search")]
        public async Task<SearchResults> Get(string query)
        {
            // Copy data from HttpContext before starting parallel tasks
            string path = HttpContext.Request.Path;
            var query1 = SearchAsync(SearchEngine.Google, query, path);
            var query2 = SearchAsync(SearchEngine.Bing, query, path);
            var query3 = SearchAsync(SearchEngine.DuckDuckGo, query, path);
    
            await Task.WhenAll(query1, query2, query3);
            
            var results1 = await query1;
            var results2 = await query2;
            var results3 = await query3;
    
            return SearchResults.Combine(results1, results2, results3);
        }
    
        private async Task<SearchResults> SearchAsync(SearchEngine engine, string query, string path)
        {
            var searchResults = SearchResults.Empty;
            try
            {
                _logger.LogInformation("Starting search query from {path}.", path);
                searchResults = await _searchService.SearchAsync(engine, query);
                _logger.LogInformation("Finishing search query from {path}.", path);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed query from {path}", path);
            }
    
            return searchResults;
        }
    }
  10. Use HttpClient instead of WebClient for HTTP requests

    master

    When making outbound HTTP requests in .NET, use HttpClient instead of the legacy WebClient API. WebClient is considered deprecated and has been superseded by HttpClient. Always prefer asynchronous methods provided by HttpClient to avoid blocking threads.

    static readonly HttpClient client = new HttpClient();
    
    public async Task<string> DoSomethingAsync()
    {
        return await client.GetStringAsync("http://www.google.com");
    }
  11. Prefer async/await over directly returning Task

    master

    While directly returning a Task is slightly more performant because it avoids the async state machine, it is generally better to use the async/await keywords. Using async/await provides several benefits:

    • Exception Normalization: Asynchronous and synchronous exceptions are always normalized to be asynchronous.
    • Easier Debugging: Diagnostics and debugging of hangs are more straightforward.
    • Safety: Exceptions are automatically wrapped in the returned Task rather than surprising the caller.
    • AsyncLocals: Prevents AsyncLocal values from leaking out of the method.
    • Maintainability: Makes it easier to add logic like using blocks later.
    // BAD: Directly returning the Task
    public Task<int> DoSomethingAsync()
    {
        return CallDependencyAsync();
    }
    
    // GOOD: Using async/await
    public async Task<int> DoSomethingAsync()
    {
        return await CallDependencyAsync();
    }
  12. Cache asynchronous operations with ConcurrentDictionary

    master

    When caching the results of asynchronous operations in a ConcurrentDictionary, do not use .Result inside GetOrAdd, as this causes thread-pool starvation.

    Instead, store the Task<T> itself. To prevent the same expensive operation from being triggered multiple times if GetOrAdd runs the delegate concurrently, use an AsyncLazy<T> pattern.

    public class PersonController : Controller
    {
       private AppDbContext _db;
       // Use AsyncLazy to ensure the delegate runs only once
       private static ConcurrentDictionary<int, AsyncLazy<Person>> _cache = new ConcurrentDictionary<int, AsyncLazy<Person>>();
       
       public PersonController(AppDbContext db) => _db = db;
       
       public async Task<IActionResult> Get(int id)
       {
           var person = await _cache.GetOrAdd(id, (key) => new AsyncLazy<Person>(() => _db.People.FindAsync(key))).Value;
           return Ok(person);
       }
       
       private class AsyncLazy<T> : Lazy<Task<T>>
       {
          public AsyncLazy(Func<Task<T>> valueFactory) : base(valueFactory) { }
       }
    }