agentleFS
AllThingsSmittyAllThingsSmitty/proven-skills/skills/dotnet-async/SKILL.md

dotnet-async

Skill1 starsChanged 27 days agoMITView on GitHub

C# async/await patterns advisor. Always use this skill when working with async/await in C#, Task, ValueTask, CancellationToken, ConfigureAwait, or deadlocks. Use when the user asks "why is my async code deadlocking", ".Result causing deadlock", "ConfigureAwait(false)", "async void vs async Task", "when to use ValueTask", "how do I cancel an async operation", "thread CancellationToken through async chain", or "can I call async from sync in C#". Read this skill before advising on any C# async or Task question.

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 1 is distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If you genuinely need to call async from sync (e.g., a console app entry point or legacy code), use Task.Run(() => …

    line 25
  2. Distinctive:

    Don't store it in a field and await it multiple times

    line 119
  3. Very specific:

    Don't use it unless profiling shows Task allocation is a bottleneck.

    line 120
  4. Very specific:

    Use Task.WhenAll for concurrent independent operations:

    line 124

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

C# Async Patterns

C#'s async/await model is powerful but has a handful of well-known pitfalls. Most production async bugs fall into three categories: deadlocks from blocking on async code, unobserved exceptions from async void, and missing cancellation support. Know these cold.

Never block on async code

Calling .Result or .Wait() on a Task blocks the current thread and can deadlock in contexts with a synchronization context (WinForms, WPF, old ASP.NET):

// Bad: can deadlock in sync-context environments
var user = GetUserAsync(id).Result;
var data = FetchDataAsync().GetAwaiter().GetResult();

// Good: async all the way up
var user = await GetUserAsync(id);

In ASP.NET Core there is no synchronization context, so .GetAwaiter().GetResult() won't deadlock there, but it still blocks a thread pool thread, defeating the purpose of async. Make it async all the way.

If you genuinely need to call async from sync (e.g., a console app entry point or legacy code), use Task.Run(() => method()).GetAwaiter().GetResult() as a last resort, but don't normalize it.

async void is dangerous

async void methods can't be awaited. If they throw, the exception is unobservable and crashes the process (or is silently swallowed depending on the host). Use async Task everywhere except event handlers.

// Bad: exception from SendEmailAsync is unobservable
async void NotifyUser(User user)
{
    await SendEmailAsync(user.Email);
}

// Good: exception can be observed by the caller
async Task NotifyUserAsync(User user)
{
    await SendEmailAsync(user.Email);
}

The one legitimate use for async void is event handlers, where the delegate signature requires void:

button.Click += async (sender, e) =>
{
    await DoWorkAsync();
};

Even then, wrap the body in a try/catch to prevent unobserved exceptions from surfacing.

ConfigureAwait(false)

In library code, use ConfigureAwait(false) on every await to avoid capturing the synchronization context. This prevents deadlocks when library consumers call your code from a context-aware environment.

// Library code
public async Task<string> FetchAsync(string url)
{
    var response = await httpClient.GetAsync(url).ConfigureAwait(false);
    return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}

In application code (ASP.NET Core controllers, Blazor, etc.), ConfigureAwait(false) is not required: ASP.NET Core has no sync context. Omitting it is fine and reduces noise.

Rule of thumb: library authors need it; application developers usually don't.

CancellationToken: thread it through everything

Accept a CancellationToken in every async method and pass it to every downstream async call. This enables proper cooperative cancellation: the request is cancelled, the database query stops, the HTTP call aborts.

// Bad: no cancellation support — keeps running after the client disconnects
public async Task<User> GetUserAsync(string id)
{
    return await db.Users.FindAsync(id);
}

// Good: propagates cancellation
public async Task<User> GetUserAsync(string id, CancellationToken ct = default)
{
    return await db.Users.FindAsync(new object[] { id }, ct);
}

In ASP.NET Core, inject CancellationToken as an action parameter; the framework provides a token that cancels when the client disconnects:

[HttpGet("{id}")]
public async Task<User> GetUser(string id, CancellationToken ct)
{
    return await userService.GetUserAsync(id, ct);
}

Task vs ValueTask

Task allocates a heap object. For hot paths where the result is often available synchronously (cache hits, simple property access), ValueTask avoids the allocation:

// ValueTask — no allocation when result is already available
public ValueTask<string> GetCachedAsync(string key)
{
    if (_cache.TryGetValue(key, out var value))
        return ValueTask.FromResult(value);   // no allocation

    return new ValueTask<string>(FetchFromDbAsync(key));
}

ValueTask rules:

  • Can only be awaited once
  • Don't store it in a field and await it multiple times
  • Don't use it unless profiling shows Task allocation is a bottleneck; Task is almost always fine

Concurrent operations

Use Task.WhenAll for concurrent independent operations:

// Sequential — total time = sum
var user = await GetUserAsync(id, ct);
var orders = await GetOrdersAsync(id, ct);

// Concurrent — total time = slowest
var (user, orders) = await (GetUserAsync(id, ct), GetOrdersAsync(id, ct));
// Or:
await Task.WhenAll(GetUserAsync(id, ct), GetOrdersAsync(id, ct));

Task.WhenAll throws AggregateException if any task faults. To get partial results when some may fail:

var tasks = new[] { FetchA(ct), FetchB(ct), FetchC(ct) };
await Task.WhenAll(tasks);  // awaiting after WhenAll unwraps the first exception
// Check each task individually for partial results

Async in constructors

Constructors can't be async. Use a static factory method:

public class Repository
{
    private readonly DbConnection _connection;

    private Repository(DbConnection connection) => _connection = connection;

    public static async Task<Repository> CreateAsync(string connStr, CancellationToken ct = default)
    {
        var conn = new DbConnection(connStr);
        await conn.OpenAsync(ct);
        return new Repository(conn);
    }
}

var repo = await Repository.CreateAsync(connStr, ct);

What to watch for in code review

  • .Result or .Wait(): deadlock risk and thread waste
  • async void outside of event handlers: unobservable exceptions
  • Async methods that don't accept CancellationToken
  • await inside a loop where Task.WhenAll would work
  • Missing ConfigureAwait(false) in library projects
  • ValueTask stored in a field or awaited multiple times

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.