Skip to content

BRO1314: Asynchronous method names should end with Async

A method that returns Task, Task<T>, ValueTask, ValueTask<T> or IAsyncEnumerable<T> has a name ending in Async, as in .NET's own APIs. Like Roslynator's RCS1046 and Meziantou's MA0137. Off by default.

Example

root = true

[*.cs]
dotnet_diagnostic.BRO1314.severity = warning

Before

using System.Threading.Tasks;

internal interface IStore
{
    Task Save(int id);
}

internal class Store : IStore
{
    public Task Save(int id) => Task.CompletedTask;

    public async Task<int> Copy(int id)
    {
        await Save(id);
        return id;
    }
}

After

using System.Threading.Tasks;

internal interface IStore
{
    Task SaveAsync(int id);
}

internal class Store : IStore
{
    public Task SaveAsync(int id) => Task.CompletedTask;

    public async Task<int> CopyAsync(int id)
    {
        await SaveAsync(id);
        return id;
    }
}

Why

The suffix tells a caller the method has to be awaited, and keeps room for a synchronous Save next to SaveAsync.

Configuration

stylebro_rename_public_api = true also renames names other assemblies see (default false, see Not reported; the owner's decision in decisions.md).

How the fix works

The method is renamed everywhere in the solution, with its overrides and implementations (explicit ones too), like BRO1309's renames. A method whose name also starts with a lower-case letter gets both changes in one rename (load -> LoadAsync) while BRO1309 is on.

"Asynchronous" means the declared return type: Task, Task<T>, ValueTask, ValueTask<T> (System.Threading.Tasks) or IAsyncEnumerable<T>. Not the async keyword (a method can return a Task without it), and not other awaitable types (GetAwaiter()): a custom awaitable says little about how callers use it. async void methods aren't reported.

This renames public API: callers in other repositories break, so the rule is off by default.

Not reported

  • Where IDE1006 is suppressed: after #pragma warning disable IDE1006 (up to its restore), or with [SuppressMessage("Style", "IDE1006")] on the declaration, its member or type, or a global one targeting them. Code that opts out of the SDK's naming rules (a file copied from another project) keeps its names.
  • A new name your SDK naming rule rejects: when the first dotnet_naming_rule that covers the symbol is enforced (IDE1006, or the rule's own severity, at suggestion or above) and asks for another style, like _camelCase for private properties, the name stays (renaming it would only trade this warning for IDE1006).
  • The name appears in code excluded by #if anywhere in the project: disabled code isn't compiled, so the rename can't follow it.
  • Public API (unless stylebro_rename_public_api = true): Methods that other assemblies see, that is public, protected or protected internal in types that are visible too. Renaming them breaks code outside the solution. Internal code is renamed, also with InternalsVisibleTo.
  • A name that nameof(...) produces: the rename would change the string (Ocelot built an HTTP header name with nameof(X_RateLimit_Limit), and a test read a private field with GetField(nameof(_components))). Looked for in the member for variables and parameters, in the type for fields; the fix also looks in the rest of the solution and leaves the warning when it finds one.
  • Names that contain Async anywhere (ReadAsyncCore, WithAsyncLock), and Main.
  • Overrides and implementations: they keep the base member's name (a library's ExecuteAsync, Dispose); the base is reported when it is in your code.
  • Methods with attributes, except a few harmless ones ([Obsolete], [MethodImpl], [DebuggerStepThrough], [DebuggerHidden], [DebuggerNonUserCode], [EditorBrowsable], [SuppressMessage], [ExcludeFromCodeCoverage], [Pure]): test methods ([Fact], [Test], [TestMethod]), [HttpGet], [JSInvokable], [OperationContract] and the like are found through their attribute, and some frameworks use the name.
  • Methods of ASP.NET controllers (a type named ...Controller, deriving from Controller/ControllerBase, or with [ApiController]) and SignalR hubs (deriving from Hub): MVC trims Async from action names, and hub clients call methods by name.
  • Event handlers: (object sender, EventArgs e) or a type derived from EventArgs.
  • Local functions, and a new name that is already taken (Load next to an existing LoadAsync).
  • The same rename guards as BRO1309: extern and partial methods, members paired by name (ShouldSerializeX).

Reported but not fixed

If dotnet format leaves a finding, StyleBro kept it on purpose. The fix looks at the whole solution, other projects included, which the analyzer can't see, and leaves a change that would break something there: the name in a string or a nameof (reflection, serialization), a member that would hide it, a use in generated code. It offers no code action for such a finding, and stylebro-migrate format lists each one with its reason (Findings kept on purpose); its --verify-no-changes doesn't fail on them. Change it by hand only after checking those uses, or suppress it or record it in the baseline.

  • The name appears as a string anywhere in the solution (GetMethod("Save")): reflection would break.
  • An implementation also implements another interface's member or a library's with the old name: renaming it would break that.
  • A derived type already has a member with the new name.

Not checked

The reverse (a method ending in Async that isn't asynchronous) isn't reported: such names are usually deliberate (IAsyncDisposable-style wrappers, a synchronous method kept next to its old async name).

Compared with StyleCop

StyleCop has no such rule. The SDK can report it with a naming rule (required_modifiers = async), but dotnet format can't fix IDE1006.

Real world (2026-10-06)

Run with every other rule and the repositories' tests (Invoke-RealWorld.ps1 -Enable BRO1314 -Tests): Jellyfin 496 findings (483 renamed, 13 kept by the string and interface guards, e.g. Stop), Polly 96 (95; PredicateResult.True kept), FFMpegCore 7, Newtonsoft.Json 3, FluentValidation 2, Serilog 1, CsvHelper 0. All compile, converge in one run, and no test that passed before fails.