AsyncFixer

repository·main·Indexed 42 days ago

https://github.com/semihokur/asyncfixer

A Roslyn analyzer for C# that detects and automatically fixes common async/await anti-patterns. It identifies unnecessary async usage, blocking calls, dangerous fire-and-forget patterns, and task type mismatches through rules such as AsyncFixer01 through AsyncFixer06. Available as a NuGet package and a Visual Studio Extension (VSIX) for versions 2017, 2019, and 2022-2026.

Tokens
3.8K
Snippets
13
Records
14
Agent score
3%

What's inside AsyncFixer

  1. Compare AsyncFixer05 and AsyncFixer06

    main

    Both rules detect task type mismatches but address different structural problems:

    AspectAsyncFixer05AsyncFixer06
    PatternTask<Task<T>> (nested task)Task<T> → Task (implicit conversion)
    ProblemAwaiting outer task doesn't wait for inner taskResult value T is silently discarded
    ContextTaskFactory.StartNew with async lambdasLambda assignments to Func<Task>
    FixUse Unwrap() or Task.Run()Change delegate type to Func<Task<T>>
    // AsyncFixer05 - nested task: Task<Task>
    Task task = Task.Factory.StartNew(() => DelayAsync());
    
    // AsyncFixer06 - result discarded: Task<string> converted to Task
    Func<Task> fn = () => GetDataAsync();  // GetDataAsync returns Task<string>
  2. Understand and apply AsyncFixer01 (Unnecessary async/await usage)

    main

    AsyncFixer01 identifies methods that use async/await solely to pass through a task. Eliding (removing) these keywords improves performance by avoiding state machine allocations and reducing GC pressure.

    When to elide async/await (Follow AsyncFixer01):

    Use direct passthrough when the method simply returns the result of another task without additional logic.

    When to keep async/await (Suppress AsyncFixer01):

    Do not elide async/await in these scenarios:

    1. Inside using blocks: To ensure resources are disposed of only after the task completes.
    2. Inside try/catch blocks: To ensure exceptions from the awaited task are caught locally.
    3. Multiple awaits: When the method contains more than one await expression.
    4. Stack trace requirements: When you need the method to appear in the exception stack trace.
    // ❌ Unnecessary overhead - allocates state machine
    async Task<User> GetUserAsync(int id) => await _repository.GetUserAsync(id);
    
    // ✅ Direct passthrough - no allocation, same behavior
    Task<User> GetUserAsync(int id) => _repository.GetUserAsync(id);
    
    // Keep async/await here - inside using block
    async Task ProcessAsync()
    {
        using var stream = new FileStream("file.txt", FileMode.Open);
        await stream.ReadAsync(buffer);  // Must await before disposal
    }
    
    // Keep async/await here - exception handling
    async Task<Data> GetDataAsync()
    {
        try {
            return await _client.GetAsync();
        } catch (HttpException) {
            return Data.Empty;
        }
    }
  3. Suppress AsyncFixer warnings

    main

    If you have a valid reason to bypass a rule (e.g., for debugging or specific resource management), you can suppress warnings using the following methods:

    1. Inline suppression (Single occurrence)

    Use #pragma directives to wrap the specific code block.

    2. Attribute suppression (Method or Class level)

    Use the SuppressMessage attribute from System.Diagnostics.CodeAnalysis.

    3. EditorConfig (Project-wide)

    Set the severity to none in your .editorconfig file for specific rules.

    4. GlobalSuppressions.cs (Assembly level)

    Apply a global suppression to the entire assembly using the [assembly: SuppressMessage] attribute.

    // 1. Inline suppression
    #pragma warning disable AsyncFixer01
    async Task<int> Method() => await SomeTaskAsync();
    #pragma warning restore AsyncFixer01
    
    // 2. Attribute suppression
    [System.Diagnostics.CodeAnalysis.SuppressMessage("AsyncUsage", "AsyncFixer01")]
    async Task<int> Method() => await SomeTaskAsync();
    
    // 4. GlobalSuppressions.cs
    [assembly: SuppressMessage("AsyncUsage", "AsyncFixer01", Justification = "Team preference")]
    // 3. EditorConfig
    [*.cs]
    dotnet_diagnostic.AsyncFixer01.severity = none
  4. Install AsyncFixer via NuGet (Recommended)

    main

    To use AsyncFixer as a project-local analyzer that participates in builds and travels with your source control, install it as a NuGet package. This ensures the same rules are applied across your entire team.

    Use the .NET CLI:

    dotnet add package AsyncFixer

    Or use the Package Manager Console in Visual Studio:

    Install-Package AsyncFixer
    dotnet add package AsyncFixer
  5. Configure AsyncFixer rule severity in .editorconfig

    main

    You can control how AsyncFixer rules are reported by setting their severity in your .editorconfig file using the dotnet_diagnostic.<RuleID>.severity key.

    Available severity levels:

    • none: Disables the rule.
    • suggestion: Reports the rule as a suggestion.
    • warning: (Default behavior for many analyzers)
    • error: Treats the rule violation as a build error.
    [*.cs]
    # Disable a rule
    dotnet_diagnostic.AsyncFixer01.severity = none
    
    # Treat as error
    dotnet_diagnostic.AsyncFixer02.severity = error
    
    # Treat as suggestion
    dotnet_diagnostic.AsyncFixer03.severity = suggestion
  6. Suppress AsyncFixer rules in C# code

    main

    If you need to ignore a specific instance of an AsyncFixer rule, use standard C# #pragma directives to disable and then restore the specific rule ID.

    #pragma warning disable AsyncFixer01
    async Task<int> Method() => await SomeTaskAsync();
    #pragma warning restore AsyncFixer01
  7. Review AsyncFixer analyzer rules in Release 2.0.0

    main

    Release 2.0.0 introduced several new rules categorized under AsyncUsage. All rules in this release are emitted with a Warning severity by default. Use these Rule IDs to configure your analysis settings (e.g., to treat specific rules as errors or disable them).

    | Rule ID | Category | Severity | Notes |
    |---------|----------|----------|-------|
    | AsyncFixer01 | AsyncUsage | Warning | Detects unnecessary async/await in async methods |
    | AsyncFixer02 | AsyncUsage | Warning | Detects blocking calls inside async methods |
    | AsyncFixer03 | AsyncUsage | Warning | Detects async void methods and delegates |
    | AsyncFixer04 | AsyncUsage | Warning | Detects unawaited async calls in using blocks |
    | AsyncFixer05 | AsyncUsage | Warning | Detects Task<Task> nested task issues |
    | AsyncFixer06 | AsyncUsage | Warning | Detects Task<T> to Task implicit conversion |
  8. AsyncFixer01: Unnecessary async/await usage

    main

    Identifies methods that use the async/await keywords but do not actually require them (e.g., they simply return the result of another task). Removing these reduces the overhead of the async state machine.

    Note: Do NOT remove async/await if the method contains:

    • Exception handling (try/catch) around the await.
    • Multiple await statements.
    • Code inside a using or try block that depends on the task completion.
    // ❌ Bad: Unnecessary async/await
    async Task<int> GetValueAsync()
    {
        return await _cache.GetAsync(key);
    }
    
    // ✅ Good: Return task directly
    Task<int> GetValueAsync()
    {
        return _cache.GetAsync(key);
    }
  9. AsyncFixer03: Fire-and-forget async void methods and delegates

    main

    Identifies async void methods and delegates that are not event handlers. async void is dangerous because unhandled exceptions will crash the process and they cannot be awaited by the caller. Use async Task instead.

    // ❌ Bad: async void - exceptions will crash the process
    async void ProcessDataAsync()
    {
        await Task.Delay(1000);
        throw new Exception("Oops!"); // Crashes the app!
    }
    
    // ✅ Good: async Task - exceptions can be caught
    async Task ProcessDataAsync()
    {
        await Task.Delay(1000);
        throw new Exception("Oops!"); // Can be caught by caller
    }
  10. AsyncFixer05: Downcasting from a nested task to an outer task

    main

    Detects dangerous downcasting when mixing async/await with older threading APIs like TaskFactory.StartNew. This often results in a Task<Task> where the outer await completes immediately without waiting for the inner task.

    Recommended Fixes:

    1. Use Task.Run() (Preferred).
    2. Use .Unwrap() on the task.
    3. Use a double await: await (await Task.Factory.StartNew(...));
    // ❌ Bad: StartNew returns Task<Task>, outer await completes immediately
    async Task ProcessAsync()
    {
        Console.WriteLine("Hello");
        await Task.Factory.StartNew(() => Task.Delay(1000)); // Returns Task<Task>!
        Console.WriteLine("World");  // Prints immediately, doesn't wait 1 second
    }
    
    // ✅ Good: Use Task.Run()
    async Task ProcessAsync()
    {
        Console.WriteLine("Hello");
        await Task.Run(() => Task.Delay(1000));
        Console.WriteLine("World");  // Waits 1 second
    }
  11. AsyncFixer02: Blocking calls inside async methods

    main

    Detects potentially long-running or blocking operations used inside async methods that have asynchronous equivalents. Using blocking calls in async contexts can lead to deadlocks and thread pool starvation.

    Common Replacements:

    Blocking CallAsync Replacement
    Task.Wait()await task
    Task.Resultawait task
    Task.WaitAll()await Task.WhenAll()
    Task.WaitAny()await Task.WhenAny()
    Thread.Sleep()await Task.Delay()
    StreamReader.ReadToEnd()await StreamReader.ReadToEndAsync()
    // ❌ Bad: Blocking call can cause deadlocks
    async Task ProcessAsync()
    {
        var result = GetDataAsync().Result;  // Blocks!
        Thread.Sleep(1000);                   // Blocks!
    }
    
    // ✅ Good: Use async equivalents
    async Task ProcessAsync()
    {
        var result = await GetDataAsync();
        await Task.Delay(1000);
    }