modern-csharp-coding-standards について
C#コードの新規作成やリファクタリングに取り組む開発者向けのスキルです。API、ドメインモデル、非同期処理、性能を考慮した設計や実装の規範をまとめています。
主な機能
- 01不変型を基本とする設計
- 02Nullableと型安全性の指針
- 03非同期処理とキャンセル規約
- 04SpanとMemoryによる性能パターン
- 05API設計と命名規則
活用シーン
- 01C#コードの新規作成や改修
- 02ライブラリやサービスの公開API設計
- 03性能が重要な処理経路の最適化
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill modern-csharp-coding-standardsインストール後、Claude Code で/modern-csharp-coding-standardsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
reference/anti-patterns.mdapi-design.mderror-handling.mdlanguage-patterns.mdperformance-patterns.md詳細
モダンな C# コーディング規約
このスキルを使用する場面
次の場合にこのスキルを使用します:
- 新しい C# コードを記述するとき、または既存のコードをリファクタリングするとき
- ライブラリやサービスのパブリック API を設計するとき
- パフォーマンスが重要なコードパスを最適化するとき
- 強い型付けを備えたドメインモデルを実装するとき
- async/await を多用するアプリケーションを構築するとき
- バイナリデータ、バッファー、または高スループットのシナリオを扱うとき
基本原則
- デフォルトで不変にする -
record型とinitのみのプロパティを使用します - 型安全性 - Nullable 参照型と値オブジェクトを活用します
- モダンなパターンマッチング -
switch式とパターンを積極的に使用します - あらゆる場面で非同期処理を使用する - 適切なキャンセル機能を備えた非同期 API を優先します
- 割り当てをゼロにするパターン - パフォーマンスが重要なコードでは
Span<T>とMemory<T>を使用します - API 設計 - 抽象型を受け取り、適切に具体的な型を返します
- 継承よりコンポジションを優先する - 抽象基底クラスを避け、コンポジションを優先します
- 値オブジェクトには構造体を使用する - 値オブジェクトには
readonly record structを使用します
命名規則
一般規則
| 要素 | 規則 | 例 |
|---|---|---|
| 名前空間 | PascalCase、ドット区切り | MyCompany.MyProduct.Core |
| クラス、レコード、構造体 | PascalCase | OrderService、OrderSummary |
| インターフェイス | I + PascalCase | IOrderRepository |
| メソッド | PascalCase | GetOrderAsync |
| プロパティ | PascalCase | OrderDate |
| イベント | PascalCase | OrderCompleted |
| パブリック定数 | PascalCase | MaxRetryCount |
| プライベートフィールド | _camelCase | _orderRepository |
| パラメーター、ローカル変数 | camelCase | orderId、totalAmount |
| 型パラメーター | T または T + PascalCase | T、TKey、TValue |
| 列挙型メンバー | PascalCase | OrderStatus.Pending |
非同期メソッドの命名
非同期メソッドには Async を付けます:
public Task<Order> GetOrderAsync(int id);
public ValueTask SaveChangesAsync(CancellationToken ct);
Exception: Event handlers and interface implementations where the framework does not use the `Async` suffix (e.g., ASP.NET Core middleware `InvokeAsync` is already named by the framework).
ブール値の命名
ブール値には is、has、can、should などを先頭に付けます:
public bool IsActive { get; set; }
public bool HasOrders { get; }
public bool CanDelete(Order order);
コレクションの命名
コレクションには複数形の名詞を使用します:
public IReadOnlyList<Order> Orders { get; }
public Dictionary<string, int> CountsByName { get; }
ファイル構成
1 ファイルに 1 つの型
各トップレベルの型(クラス、レコード、構造体、インターフェイス、列挙型)は、それぞれ型と完全に同じ名前のファイルに記述します。ネストされた型は、包含する型と同じファイルに記述します。
OrderService.cs -> public class OrderService
IOrderRepository.cs -> public interface IOrderRepository
OrderStatus.cs -> public enum OrderStatus
OrderSummary.cs -> public record OrderSummary
ファイルスコープの名前空間
常にファイルスコープの名前空間(C# 10 以降)を使用します:
namespace MyApp.Services;
public class OrderService { }
using ディレクティブ
using ディレクティブはファイルの先頭、名前空間の外に配置します。<ImplicitUsings>enable</ImplicitUsings>(モダンな .NET ではデフォルト)が有効な場合、一般的な名前空間はすでにインポートされています。
using ディレクティブの順序:
System.*名前空間- サードパーティの名前空間
- プロジェクトの名前空間
コードスタイル
波かっこ
1 行の本体であっても、制御フローには必ず波かっこを使用します:
if (order.IsValid)
{
Process(order);
}
式形式のメンバー
単一の式で構成されるメンバーには式形式を使用します:
public string FullName => $"{FirstName} {LastName}";
public override string ToString() => $"Order #{Id}";
var の使用
右辺から型が明らかな場合は var を使用します:
var orders = new List<Order>();
var customer = GetCustomerById(id);
IOrderRepository repo = serviceProvider.GetRequiredService<IOrderRepository>();
decimal total = CalculateTotal(items);
null の処理
null チェックよりもパターンマッチングを優先します:
if (order is not null) { }
if (order is { Status: OrderStatus.Active }) { }
var name = customer?.Name ?? "Unknown";
var orders = customer?.Orders ?? [];
items ??= [];
文字列の処理
連結や string.Format よりも文字列補間を優先します:
var message = $"Order {orderId} totals {total:C2}";
var json = $$"""
{
"id": {{orderId}},
"name": "{{name}}"
}
""";
アクセス修飾子
デフォルトに頼らず、常にアクセス修飾子を明示します:
public class OrderService
{
private readonly IOrderRepository _repo;
internal void ProcessBatch() { }
}
修飾子の順序
access (public/private/protected/internal) -> static -> extern -> new ->
virtual/abstract/override/sealed -> readonly -> volatile -> async -> partial
public static readonly int MaxSize = 100;
protected virtual async Task<Order> LoadAsync() => await repo.GetDefaultAsync();
public sealed override string ToString() => Name;
型の設計
デフォルトでクラスを sealed にする
継承を想定して設計していないクラスには sealed を指定します。これによりパフォーマンスが向上し(仮想呼び出しの非仮想化)、意図が明確になります:
public sealed class OrderService(IOrderRepository repo)
{
}
基底クラスとして明示的に設計する場合に限り、クラスを sealed にしないでください。
継承よりコンポジションを優先する
public sealed class OrderProcessor(IValidator validator, INotifier notifier)
{
public async Task ProcessAsync(Order order)
{
await validator.ValidateAsync(order);
await notifier.NotifyAsync(order);
}
}
インターフェイスの分離
インターフェイスは目的を絞ったものにします。1 つの大きなインターフェイスより、複数の小さなインターフェイスを優先します:
public interface IOrderReader
{
Task<Order?> GetByIdAsync(int id, CancellationToken ct = default);
Task<IReadOnlyList<Order>> GetAllAsync(CancellationToken ct = default);
}
public interface IOrderWriter
{
Task<Order> CreateAsync(Order order, CancellationToken ct = default);
Task UpdateAsync(Order order, CancellationToken ct = default);
}
言語パターン
詳しいガイダンスについては、言語パターンを参照してください:
- 不変データにはレコードを使用する(C# 9 以降)
- 値オブジェクトには readonly record struct を使用する
- パターンマッチング(C# 8~12)
- Nullable 参照型(C# 8 以降)
- 継承よりコンポジションを優先する
パフォーマンスパターン
詳しいガイダンスについては、パフォーマンスパターンを参照してください。
- Async/Await のベストプラクティス
- ゼロアロケーションコードのための Span と Memory
API 設計原則
詳しいガイダンスについては、API 設計原則を参照してください。
- 抽象を受け取り、適切に具体的な型を返す
- メソッドシグネチャのベストプラクティス
エラー処理
詳しいガイダンスについては、エラー処理を参照してください。
- Result 型パターン(レールウェイ指向プログラミング)
テストパターン
public record OrderBuilder
{
public OrderId Id { get; init; } = OrderId.New();
public CustomerId CustomerId { get; init; } = CustomerId.New();
public Money Total { get; init; } = new Money(100m, "USD");
public IReadOnlyList<OrderItem> Items { get; init; } = Array.Empty<OrderItem>();
public Order Build() => new(Id, CustomerId, Total, Items);
}
[Fact]
public void CalculateDiscount_LargeOrder_AppliesCorrectDiscount()
{
var baseOrder = new OrderBuilder().Build();
var largeOrder = baseOrder with { Total = new Money(1500m, "USD") };
var discount = _service.CalculateDiscount(largeOrder);
discount.Should().Be(new Money(225m, "USD"));
}
[Theory]
[InlineData("ORD-12345", true)]
[InlineData("INVALID", false)]
public void TryParseOrderId_VariousInputs_ReturnsExpectedResult(
string input, bool expected)
{
var result = OrderIdParser.TryParse(input.AsSpan(), out var orderId);
result.Should().Be(expected);
}
[Fact]
public void Money_Add_SameCurrency_ReturnsSum()
{
var money1 = new Money(100m, "USD");
var money2 = new Money(50m, "USD");
var result = money1.Add(money2);
result.Should().Be(new Money(150m, "USD"));
}
[Fact]
public void Money_Add_DifferentCurrency_ThrowsException()
{
var usd = new Money(100m, "USD");
var eur = new Money(50m, "EUR");
var act = () => usd.Add(eur);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*different currencies*");
}
CancellationToken の規約
非同期メソッドでは、最後のパラメーターとして CancellationToken を受け取ります。省略可能なトークンの既定値には default を使用します。
public async Task<Order> GetOrderAsync(int id, CancellationToken ct = default)
{
return await _repo.GetByIdAsync(id, ct);
}
トークンは常に後続の非同期呼び出しに渡してください。受け取った CancellationToken を無視してはいけません。
XML ドキュメント
公開 API には XML ドキュメントを追加してください。簡潔に記述します。
/// <summary>
/// Retrieves an order by its unique identifier.
/// </summary>
/// <param name="id">The order identifier.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The order, or <see langword="null"/> if not found.</returns>
public Task<Order?> GetByIdAsync(int id, CancellationToken ct = default);
次のものには XML ドキュメントを追加しないでください。
- Private または internal のメンバー(ライブラリの
InternalsVisibleToAPI を除く) - 自明なメンバー(例:
public string Name { get; }) - テストメソッド
リフレクションベースのメタプログラミングを避ける
詳しいガイダンスについては、アンチパターンを参照してください。
- AutoMapper、Mapster など、リフレクションベースのライブラリを避ける理由
- 代わりに明示的なマッピングメソッドを使用する
- 正当なリフレクションの用途で UnsafeAccessorAttribute を使用する
避けるべきアンチパターン
詳しいガイダンスについては、アンチパターンを参照してください。
- ミュータブルな DTO
- 値オブジェクトにクラスを使う
- 深い継承階層
- ミュータブルなコレクションを公開する
- CancellationToken を忘れる
- 非同期コードをブロックする
コードの構成
namespace MyApp.Domain.Orders;
public record Order(
OrderId Id,
CustomerId CustomerId,
Money Total,
OrderStatus Status,
IReadOnlyList<OrderItem> Items
)
{
public bool IsCompleted => Status is OrderStatus.Completed;
public Result<Order, OrderError> AddItem(OrderItem item)
{
if (Status is not OrderStatus.Draft)
return Result<Order, OrderError>.Failure(
new OrderError("ORDER_NOT_DRAFT", "Can only add items to draft orders"));
var newItems = Items.Append(item).ToList();
var newTotal = new Money(
Items.Sum(i => i.Total.Amount) + item.Total.Amount,
Total.Currency);
return Result<Order, OrderError>.Success(
this with { Items = newItems, Total = newTotal });
}
}
public enum OrderStatus
{
Draft,
Submitted,
Processing,
Completed,
Cancelled
}
public record OrderItem(
ProductId ProductId,
Quantity Quantity,
Money UnitPrice
)
{
public Money Total => new(
UnitPrice.Amount * Quantity.Value,
UnitPrice.Currency);
}
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
}
public readonly record struct OrderError(string Code, string Message);
アナライザーによる強制
標準を自動的に適用するには、これらのアナライザーを Directory.Build.props または .editorconfig で設定します。
<PropertyGroup>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisLevel>latest-all</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
C# スタイルに関する主要な .editorconfig ルール:
[*.cs]
csharp_style_namespace_declarations = file_scoped:warning
csharp_prefer_braces = true:warning
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
dotnet_style_require_accessibility_modifiers = always:warning
csharp_style_prefer_pattern_matching = true:suggestion
ベストプラクティスのまとめ
推奨事項
- DTO、メッセージ、ドメインエンティティには
recordを使用する - 値オブジェクトには
readonly record structを使用する switch式によるパターンマッチングを活用する- Nullable 参照型を有効にし、適切に扱う
- すべての I/O 操作に async/await を使用する
- すべての非同期メソッドで
CancellationTokenを受け取る - 高パフォーマンスが求められる場面では
Span<T>とMemory<T>を使用する - 抽象(
IEnumerable<T>、IReadOnlyList<T>)を受け取る - 適切なインターフェイスまたは具象型を返す
- 想定されるエラーには
Result<T, TError>を使用する - ライブラリコードでは
ConfigureAwait(false)を使用する - 大きな割り当てには
ArrayPool<T>でバッファーをプールする - 継承よりコンポジションを優先する
- アプリケーションコードでは抽象基底クラスを避ける
禁止事項
- レコードで対応できる場合にミュータブルなクラスを使用しない
- 値オブジェクトにクラスを使用しない(
readonly record structを使用する) - 深い継承階層を作らない
- Nullable 参照型の警告を無視しない
- 非同期コードをブロックしない(
.Result、.Wait()) Span<byte>で足りる場合にbyte[]を使用しないCancellationTokenパラメーターを忘れない- API からミュータブルなコレクションを返さない
- 想定されるビジネスエラーに例外をスローしない
- ループ内で
stringの連結をしない - 大きな配列を繰り返し割り当てない(
ArrayPoolを使用する)
ナレッジソース
このスキルの規約は、以下の一般公開コンテンツに基づいています。
- Microsoft Framework Design Guidelines -- .NET の命名、型設計、API サーフェスの規約に関する標準的なリファレンスです。出典:https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/
- C# Language Design Notes (Mads Torgersen et al.) -- コーディング標準に影響する C# 言語機能の設計根拠です。このスキルに関連する主な決定事項は、ファイルスコープの名前空間(読みやすさのためにネストを減らす)、型チェックよりパターンマッチングを優先すること(表現力)、
requiredメンバー(コンパイル時の初期化の安全性)、およびvarの使用ガイドライン(読みやすさを優先)です。出典:https://github.com/dotnet/csharplang/tree/main/meetings
追加リソース
- C# 言語仕様: https://learn.microsoft.com/en-us/dotnet/csharp/
- パターンマッチング: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching
- Span と Memory: https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/
- 非同期処理のベストプラクティス: https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming
- .NET のパフォーマンスに関するヒント: https://learn.microsoft.com/en-us/dotnet/framework/performance/
- C# コーディング規約: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
- C# 識別子の命名規則: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names
- .NET の .editorconfig: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/code-style-rule-options