Skip to content

BRO1111: Generic type constraints should be on their own line

Each where clause starts its own line, one level deeper than the declaration. Replaces StyleCop SA1127.

Example

Before

using System;
using System.Collections.Generic;

public class Cache<TKey, TValue> where TKey : IComparable<TKey> where TValue : class
{
    private readonly Dictionary<TKey, TValue> items = new Dictionary<TKey, TValue>();

    public TValue Get(TKey key) => items[key];

    public static TOut Convert<TOut>(TValue value) where TOut : class => value as TOut;
}

After

using System;
using System.Collections.Generic;

public class Cache<TKey, TValue>
    where TKey : IComparable<TKey>
    where TValue : class
{
    private readonly Dictionary<TKey, TValue> items = new Dictionary<TKey, TValue>();

    public TValue Get(TKey key) => items[key];

    public static TOut Convert<TOut>(TValue value)
        where TOut : class
        => value as TOut;
}

Why

Constraints are part of the signature, but a long declaration with constraints at the end of the line hides them. On their own lines, each constraint is easy to find and to change.

How the fix works

Each where gets a new line, indented one level deeper than the line with the declaration's name. The indentation comes from .editorconfig (indent_style, indent_size, tab_width) and defaults to four spaces. For an expression-bodied member, the => after the last constraint gets its own line too, like StyleCop's fix.

Checked: classes, structs, interfaces, records, methods, delegates and local functions.

Not reported

  • A comment right before where (class Box<T> /* why */ where T : class).

Configuration

[*.cs]
stylebro_constraint_placement = own_line
  • own_line (default): like StyleCop's SA1127, as described above.
  • same_line: every where clause goes on the declaration's line, after one space:
// Before
public class Cache<TKey, TValue>
    where TKey : IComparable<TKey>
    where TValue : class
{
}

// After
public class Cache<TKey, TValue> where TKey : IComparable<TKey> where TValue : class
{
}

With same_line, a declaration's clauses are joined all together or not at all (and only then reported). They aren't when:

  • the joined line, up to the last clause's end, would be longer than max_line_length. A body after the clauses ({ }, => ...) isn't counted. Without max_line_length there is no limit, so set it when you use same_line.
  • a clause spans several lines,
  • anything but whitespace sits before a clause (a comment, a directive).

The => of an expression body stays where it is (where T : class then => ... on the next line keeps that line; BRO1521 leaves an => after a constraint alone). A ) on its own line before the first clause that BRO1110 moves to the last parameter moves in the same edit. The joined line is measured as the other fixes in the same run leave it, when those rules are on: BRO1404/BRO1007 add an access modifier, BRO1109 moves ( up, BRO1108 puts each parameter on its own line, BRO1110 moves ). So the decision doesn't depend on which fix runs first. Not counted: renames (BRO1304, BRO1305, ...) and rewrites inside the line (BRO1115 int?, BRO1123 tuple syntax, ...), whose results depend on checks made elsewhere (a rename can be skipped). Right at the limit, one of those can still leave a join for a second dotnet format run, or a joined line a few characters too long.

Compared with StyleCop

Same results as StyleCop, with the same fixed output. StyleCop has no same_line setting; teams that prefer it turn SA1127 off.