Skip to content

BRO1001: Members should be ordered

Members of a class, struct, record or interface, and the types and namespaces of a namespace or file, should appear in a fixed order: by kind, then accessibility, then constant/static/readonly. Replaces StyleCop SA1201, SA1202, SA1203, SA1204 and SA1214.

Example

Before

using System.Collections.Generic;

public class Order
{
    public void Ship()
    {
    }

    private readonly List<string> _lines = new();

    public Order(string id)
    {
        Id = id;
    }

    public string Id { get; }
}

After

using System.Collections.Generic;

public class Order
{
    private readonly List<string> _lines = new();

    public Order(string id)
    {
        Id = id;
    }

    public string Id { get; }

    public void Ship()
    {
    }
}

Why

When every type follows the same order, you know where to look: state first, then construction, then the API. It also keeps reviews focused, because nobody argues about where a new member goes.

The order

Members are sorted by these keys, in this order:

  1. Kind: field, constructor, finalizer, delegate, event, enum, interface, property, indexer, conversion, operator, extension, method, struct, class
  2. Accessibility: public, internal, protected internal, protected, private protected, private. A static constructor counts as public, like in StyleCop.
  3. Constants before other fields
  4. Static before instance
  5. Readonly fields before mutable fields

Members with equal keys keep their existing relative order. These are StyleCop's defaults.

C# 14 extension blocks (extension(string s) { ... }) are the kind extension, right before the methods, and their members are sorted inside them by the same keys (a member without an access modifier is private there, like anywhere in a class). StyleCop has no opinion on them; before, a type with an extension block wasn't sorted at all.

public static class TextExtensions
{
    private const int Limit = 10;

    public static string Name => nameof(TextExtensions);

    extension(string s)
    {
        public int Size => s.Length;

        public bool IsShort() => s.Length < Limit;
    }

    public static int Twice(int x) => x * 2;
}

In a namespace (block or file-scoped) or at the top of a file, the same keys order its namespaces and types: namespaces first, then delegates, enums, interfaces, structs and classes (records count as classes or structs). A type without an access modifier is internal there, and a part of a partial type without one counts as what its other parts declare (so the order doesn't change once BRO1007 adds the modifier).

Configuration

[*.cs]
stylebro_member_order = field, constructor, property, method
stylebro_member_access_order = public, internal, protected, private
stylebro_member_constants_first = true
stylebro_member_static_first = true
stylebro_member_readonly_first = true
stylebro_keep_overloads_together = false   # true, false, same_access

Kinds and accessibilities you leave out keep their default relative order, after the ones you list. A configured list written before extension blocks existed (without extension) puts them after every listed kind. conversion means conversion operators (implicit operator/explicit operator); operator means all other operators.

Overloads together

stylebro_keep_overloads_together = true keeps methods with the same name next to each other. The group goes where its first overload goes under the keys above (the one that sorts first; of equal ones, the one that comes first), and the other overloads follow it in their own order:

public void Load(int id) { }    // public: the group's place
private void Load(string name) { }
public void Save() { }

With the default false, the private Load goes after every public method, like in StyleCop. Only methods are grouped, within their region: constructors and indexers are kinds of their own, so they are always together, and operators aren't grouped by their symbol (pairs like == and != stay as written). An explicit interface implementation groups with the other implementations of that interface's method, not with a public method of the same name.

stylebro_keep_overloads_together = same_access is exactly Sonar's S4136: only overloads with the same accessibility, both static or both not, and both abstract or both not, form a group. The usual public void Dispose() and private void Dispose(bool disposing) may then sit apart (S4136 doesn't flag them), each sorted by its own access, while two public Find overloads stay together:

public void Dispose() { Dispose(true); }
public int Find(int id) => id;
public int Find(string name) => name.Length;
private void Dispose(bool disposing) { }

How the fix works

The fix sorts the whole type, reported once per type on the first member that is out of place. The blank-line layout stays in place, while comments, XML docs and attributes move with their member. A member with a // comment above it that lands directly below another member gets a blank line above the comment, so the sort doesn't leave a comment glued to the code above it (BRO1504). Nested types are sorted too, so one dotnet format analyzers --diagnostics BRO1001 run is enough.

Before sorting, the fix makes what BRO1505 and BRO1509 would: the blank lines BRO1505 wants between the type's members, and the expansion of a type written on one line. Since blank lines stay with their place, sorting first and adding them afterwards would give other text than the other way round. With both rules off, a type on one line is sorted on that line.

If the project uses TreatWarningsAsErrors, don't exempt the rule with <WarningsNotAsErrors>BRO1001</WarningsNotAsErrors>: dotnet format then skips BRO1001 entirely and fixes nothing, although the build still reports it. Set the severity in .editorconfig instead (dotnet_diagnostic.BRO1001.severity = suggestion), or run dotnet format before building.

Regions

Members are sorted within each region: every #region or #endregion line between members is a fence, and the members between two fences are sorted on their own. The region lines, and comments above them, stay where they are, so every region keeps its members. #pragma and #nullable lines are fences too, so every member keeps the warnings and nullable context it had. The order across regions isn't checked (StyleCop's is): fixing it would mean moving members into another region. A doc comment that moves right below a region line gets a blank line above it (BRO1513).

With BRO1112 on, a type with regions between its members isn't reported: BRO1112's fix removes the regions and sorts the members across them in the same step. Sorting within the regions first could end differently (a commented member moved next to one of its kind looks like the start of a group, which the sort then leaves alone).

Not reported

  • Types with #if, #elif, #else or #endif between members, and namespaces or files with them between types. In a multi-targeted project each target framework sees other members there, and the copies of the file would need different edits. Also a doc comment above a directive, which would stay with the position instead of its member.
  • Members out of order across regions (see Regions).
  • Types whose member order the runtime sees, when sorting would change it: instance fields of a struct (sequential layout is the default) or of a type with [StructLayout] (memory layout for interop, unsafe code and Marshal), and fields and properties of a type with a serializer attribute on it or on a member (Json.NET, DataContract, XmlSerializer, MessagePack, protobuf write members in declaration order). Found on Newtonsoft.Json, whose tests compare JSON text. Other members of such a type are still sorted when that keeps the order of these. A class that is serialized without any attribute can't be recognized and is sorted (StyleCop's fix reorders those too).
  • Containers where a member led by a group comment would move. Comments move with the member below them, which is wrong for a // comment that introduces a group: it would end up above part of the group, away from the rest. A comment counts as a group comment when the next member is of the same kind (found in Scrutor):
// Test interfaces and classes
public interface IService { }
public interface IOtherService { }

public class Service : IService { }

Sorted, the comment would sit above the interfaces only. Blank lines don't decide it: other rules add and remove them in the same dotnet format run. A comment above a member whose next member is of another kind moves with that member, and a group comment whose member keeps its place doesn't stop the sort elsewhere in the type. - Containers where a member led by a comment below the previous member would move. A run of // lines right below a line ending in ;, followed by a blank line, describes the member above it (found in eShop), although the compiler attaches it to the member below. BRO1504 and BRO1506 leave its blank lines alone, so it stays put, and the sort would carry it away with the wrong member:

private static RedisKey BasketKeyPrefix = "/basket/"u8.ToArray();
// note on UTF8 here: library limitation (to be fixed) - prefixes are more efficient as blobs

private static RedisKey GetBasketKey(string userId) => BasketKeyPrefix.Append(userId);
- Files whose first type carries the file header: with no using or namespace before it, the header comment belongs to the first type and would move with it. - Files with top-level statements. - Types where sorting would change what field initializers compute. Initializers run in source order, so moving B above A here (readonly first) would make B 1 instead of 6:

public static class C
{
    public static int A = 5;
    public static readonly int B = A + 1;
}

A type is skipped when sorting would swap two static initializers, or two instance initializers, and either of them mentions a member of the type (directly or through a method), or creates an instance of the type (new() in a singleton). typeof, nameof and names on other objects (text.Length) don't count. Side effects outside the type, such as two initializers calling the same external counter, aren't detected.

Reported but not fixed

A type whose members code reads by position through reflection keeps its order when sorting would change what that code reads: GetFields, GetProperties, GetEvents or GetMembers with an index, First(), Last() or ElementAt(), directly or on a local holding the result, on typeof(T), new T(), this or a variable declared as T (GetType()), anywhere in the solution. Reflection returns members in declaration order (found on Stateless: its tests read an event's backing field as GetFields(...)[0] and a list as [1], and 5 failed after the sort). Only the fix sees the other projects, so the finding stays, with no code action; stylebro-migrate format lists it with where the read is (Findings kept on purpose). The type is matched by its simple name. Other members are still sorted when the read ones keep their relative order (all members for GetMembers). A serializer that reads members by reflection without a positional read in the code (Json.NET's DefaultMembersSearchFlags on an attribute-less class) can't be recognized.

Compared with StyleCop

StyleCop reports every misplaced member; BRO1001 reports each type (or namespace, or file) once and fixes all of it. StyleCop doesn't skip types where sorting changes initializer results. The member order itself is the same, checked pairwise for all 14 member kinds against StyleCop 1.1.118.