Skip to content

Metanaka .NET Coding

Rule Levels

  • [Required]: 違反は必須修正として扱う。
  • [Recommended]: 原則として採用する。合理的な理由がある逸脱は許容する。
  • [Contextual]: 記載した条件が成立するときだけ適用する。根拠が不足する場合は違反ではなく確認事項として扱う。

レビュー判定に使う規則はlevel tag付きの箇条書きだけとする。tagのない文章とcode sampleは背景、理由、例を示す。

Scope

  • [Required] このskillはproduction codeとtest codeのC#表現を扱う。
  • [Required] 配置、依存方向、HTTP/Application境界、永続化設計はmetanaka-dotnet-architectureへ委ねる。
  • [Required] testの構造、命名、検証内容、test doubleはmetanaka-dotnet-testingへ委ねる。
  • [Required] observable production behaviorを変更する実行順序はmetanaka-dotnet-tddへ委ねる。
  • [Contextual] namespace、nullable、collection expression、primary constructorなど、このskillに規則がなくroot .editorconfigにも設定がない表現は、近傍のC#とrepositoryのbuild/analyzer結果に合わせる。未定義の好みをレビュー違反にしない。

複数規則を組み合わせた形を確認するときだけsamples/expression-and-control-flow/README.mdを読む。sampleは命名やdomain設計の規則を追加しない。

Formatting and Expression Bodies

  • [Required] ifforforeachwhiledousinglockは、本文が1 statementでも波括弧を省略しない。
  • [Required] 波括弧はAllman styleとし、開始・終了の波括弧をそれぞれ独立した行に置く。
  • [Required] 1行には1 statementだけを書く。
  • [Recommended] 単一式で表現でき、member名と式から意図を直接読めるmethod、constructor、operator、property、indexer、accessor、lambda、local functionはexpression body (=>)を使う。
  • [Contextual] expression bodyは物理的な1行を強制しない。式や引数が長い場合は、=>と式を読みやすい位置で複数行に分ける。
  • [Contextual] breakpointを置く必要がある逐次処理、条件分岐を式へ埋め込む処理、複数段のLINQで中間値の意味が重要な処理はblock bodyを使う。近傍codeがblock bodyで統一されているだけでは必須理由にせず、意図やdebug時の観測点が明確になるかで判断する。
  • [Required] local variable、複数statement、条件分岐、loop、複数の副作用を含むmemberはblock bodyを使う。
  • [Contextual] local variableを宣言せず、単一のLINQ queryをそのまま返すmemberはexpression bodyを使ってよい。複数のLINQ stageでも1つのqueryとして自然に読める場合は改行したexpression bodyを許容し、途中結果へ業務上の名前を付ける必要がある場合はblock bodyとlocal variableを使う。
  • [Recommended] local variableの型をコンパイラが推論できる場合は、組み込み型を含めてvarを使う。field、property、parameter、return typeなど契約の型は省略しない。
  • [Required] 使用していないusing directiveは残さない。IDE0005をwarningとして扱う。

Control Flow

  • [Recommended] guard clauseや早期終了によって、条件分岐のnestingを浅くする。
  • [Required] 複数の相互排他的な条件から値または戻り値を選ぶ場合は、独立したifを連ねずswitch式を使う。
  • [Recommended] 副作用を伴う複数分岐や、各caseで複数statementを実行する場合はswitch文を使う。値を選ぶだけの分岐をswitch文へ広げない。
  • [Contextual] 単純な二値の選択で条件演算子の方が意図を直接読める場合は?:を使ってよい。条件演算子をnestして複数分岐を表現せずswitch式を使う。
csharp
if (!customer.IsActive)
{
    return CustomerErrors.Inactive();
}

return customer.Plan switch
{
    CustomerPlan.Free => Limits.Free,
    CustomerPlan.Standard => Limits.Standard,
    CustomerPlan.Premium => Limits.Premium,
    _ => throw new ArgumentOutOfRangeException(nameof(customer.Plan)),
};

Tuple Deconstruction

  • [Recommended] named tupleの複数要素を後続処理で使う場合は、業務上の値へ名前を付けるため分解代入する。一部だけ使う場合はdiscard (_)を使う。
  • [Contextual] tuple要素をmethod argumentとして一度だけ使う場合は、memberを直接参照してよい。
  • [Contextual] discardが複数必要になるほど大きいtupleや、値の組み合わせ自体に業務上の意味があるtupleは、意味のあるresult typeへ置き換える。result typeの責務と配置はmetanaka-dotnet-architectureに従う。
csharp
var (room, round) = hostRound.Value;

一部だけ使う別の例:

csharp
var (_, round) = playerRound.Value;

EditorConfig Source of Truth

  • [Required] formatterやanalyzerの設定値を追加・変更・判定するときはroot .editorconfigを正とする。このskillへ設定値を複製せず、規則と設定が食い違う場合は差異を報告する。
  • [Required] root .editorconfigerrorに設定された規則はbuildとCIで機械的に強制する。機械的に表現できない規則は、このskillを使う実装・レビュー時に判定する。

Verification

  • [Required] C# codeを変更したら、作業範囲に最も近いprojectまたはsolutionのdotnet buildを実行し、compiler errorとanalyzer warningがないことを確認する。
  • [Contextual] test codeも変更した場合、またはproduction behaviorへ影響し得る変更の場合は、関連するdotnet testも実行する。TDD対象の実行順序はmetanaka-dotnet-tddを正とする。
  • [Required] metanaka-dotnet-tdd適用中は、同workflowのRED/GREEN test実行と最後のbuildでこのVerificationを共同充足する。各中間編集のたびに別のbuildやtestを重複実行しない。

Expected C# Shape

csharp
public bool IsActive
    => Status == WordPairStatus.Active;

public Task SaveAsync(
    WordPair wordPair,
    CancellationToken ct)
    => repository.SaveAsync(wordPair, ct);

public IReadOnlyList<Player> ActivePlayers
    => players
        .Where(player => player.IsActive)
        .OrderBy(player => player.JoinedAt)
        .ToList();

if (!category.Exists)
{
    return WordErrors.CategoryNotFound();
}

branchやlocal variableを必要とするmemberは、expression bodyへ押し込まずblock bodyにする。

csharp
public async Task<Room?> GetRoomAsync(RoomId roomId, CancellationToken ct)
{
    var snapshot = await repository.GetAsync(roomId, ct);
    if (!snapshot.Exists)
    {
        return null;
    }

    return snapshot.ToDomain();
}