mcpField
M
modern-csharp-coding-standards

modern-csharp-coding-standards

github.com/wshaddix开发86583 次安装35 周前更新
代码仓库

关于 modern-csharp-coding-standards

面向编写和重构 C# 代码的技能,涵盖 API、领域模型、异步代码与性能场景。重点提供现代 C# 的类型设计、代码风格和实践规范。

主要功能

  1. 01默认采用不可变类型
  2. 02空值安全与类型安全
  3. 03异步与取消令牌规范
  4. 04Span 和 Memory 性能实践
  5. 05API 设计与命名规范

适用场景

  1. 01新建或重构 C# 代码
  2. 02设计库或服务的公共 API
  3. 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.md
api-design.md
error-handling.md
language-patterns.md
performance-patterns.md

详细介绍

现代 C# 编码规范

何时使用此技能

在以下情况下使用此技能:

  • 编写新的 C# 代码或重构现有代码
  • 为库或服务设计公共 API
  • 优化性能关键代码路径
  • 使用强类型实现领域模型
  • 构建大量使用 async/await 的应用程序
  • 处理二进制数据、缓冲区或高吞吐量场景

核心原则

  1. 默认不可变 - 使用 record 类型和仅限 init 的属性
  2. 类型安全 - 利用可空引用类型和值对象
  3. 现代模式匹配 - 广泛使用 switch 表达式和模式
  4. 全面异步 - 优先使用支持适当取消机制的异步 API
  5. 零分配模式 - 在性能关键代码中使用 Span<T> 和 Memory<T>
  6. API 设计 - 接受抽象类型,返回适当具体的类型
  7. 组合优于继承 - 避免抽象基类,优先使用组合
  8. 将值对象定义为结构体 - 对值对象使用 readonly record struct

命名约定

一般规则
元素约定示例
命名空间PascalCase,以点分隔MyCompany.MyProduct.Core
类、记录、结构体PascalCaseOrderService、OrderSummary
接口I + PascalCaseIOrderRepository
方法PascalCaseGetOrderAsync
属性PascalCaseOrderDate
事件PascalCaseOrderCompleted
公共常量PascalCaseMaxRetryCount
私有字段_camelCase_orderRepository
参数、局部变量camelCaseorderId、totalAmount
类型参数T 或 T + PascalCaseT、TKey、TValue
枚举成员PascalCaseOrderStatus.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; }

文件组织

每个文件一个类型

每个顶级类型(类、记录、结构体、接口、枚举)都应放在单独的文件中,文件名与类型名完全相同。嵌套类型保留在其所属类型的文件中。

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 指令的顺序:

  1. System.* 命名空间
  2. 第三方命名空间
  3. 项目命名空间

代码风格

大括号

控制流始终使用大括号,即使代码块只有一行:

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);
空值处理

优先使用模式匹配,而非空值检查:

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;

类型设计

默认密封类

对非继承设计的类使用密封。这可以提升性能(去虚拟化),并明确表达设计意图:

public sealed class OrderService(IOrderRepository repo)
{
}

只有在明确将类设计为基类时,才不使用密封。

优先使用组合,而非继承
public sealed class OrderProcessor(IValidator validator, INotifier notifier)
{
    public async Task ProcessAsync(Order order)
    {
        await validator.ValidateAsync(order);
        await notifier.NotifyAsync(order);
    }
}
接口隔离

保持接口专注。优先使用多个小型接口,而非一个大型接口:

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)
  • 可空引用类型(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 文档:

  • 私有或内部成员(除非是库的 InternalsVisibleTo API)
  • 一目了然的成员(例如,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

最佳实践总结

应当
  • 使用 record 表示 DTO、消息和领域实体
  • 使用 readonly record struct 表示值对象
  • 使用 switch 表达式进行模式匹配
  • 启用并遵循可空引用类型规范
  • 对所有 I/O 操作使用 async/await
  • 在所有异步方法中接受 CancellationToken
  • 在高性能场景中使用 Span<T> 和 Memory<T>
  • 接受抽象类型(IEnumerable<T>、IReadOnlyList<T>)
  • 返回适当的接口或具体类型
  • 使用 Result<T, TError> 表示预期错误
  • 在库代码中使用 ConfigureAwait(false)
  • 对大型分配使用 ArrayPool<T> 池化缓冲区
  • 优先使用组合而非继承
  • 避免在应用代码中使用抽象基类
不应当
  • 在适合使用记录时,不要使用可变类
  • 不要用类表示值对象(应使用 readonly record struct)
  • 不要创建深层继承层次结构
  • 不要忽略可空引用类型警告
  • 不要阻塞异步代码(.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

其他资源

评论

支持 Markdown