C# 代码规范实战指南:Conductor 模板驱动的 .NET 编码约定与最佳实践
【免费下载链接】agentsMulti-harness agentic plugin marketplace for Claude Code, Codex, Cursor, OpenCode, GitHub Copilot, and Google Antigravity项目地址: https://gitcode.com/GitHub_Trending/agents24/agents
Conductor(Context-Driven Development 插件)在/conductor:setup初始化项目时会基于templates/code_styleguides/下的模板为选定语言生成项目级编码规范,其中csharp.md正是面向 .NET/C# 的标准风格指南。本篇以该模板为骨架,逐节讲解 C# 的命名约定、异步编程、LINQ、依赖注入、单元测试与代码组织规范,并辅以仓库中 dotnet-contribution 插件(README.md)的源码资产交叉印证,帮助你在自己的 .NET 项目中快速落地一套既符合社区惯例、又可持续约束 AI 协作产出的编码基线。
该规范在 Conductor 项目中的角色
在 Conductor 生成的每个项目上下文里,语言级编码规范都保存在conductor/code_styleguides/目录下。根据 setup.md 的描述,交互式初始化(Section 5:Code Style Guides)会先询问“要为哪些语言生成风格指南”(如 TypeScript/JavaScript、Python、Go、Rust、全部检测到的语言或跳过),并询问是否引入已有 lint/format 配置;随后在 Artifact Generation 阶段从$CLAUDE_PLUGIN_ROOT/templates/code_styleguides/复制对应语言模板到目标项目。
因此,本模板实际承担两个角色:
- 作为「项目级规范模板」,被 conductor/templates/index.md 的 Style Guides 导航表登记为 “C# conventions”,与 general.md(通用原则)、typescript、javascript、python、go、dart、html-css 等并行;
- 作为「可执行规范清单」,在后续
/conductor:new-track、/conductor:implement的开发流程中约束生成的 C# 代码风格。
值得注意的是,模板中的约定并非孤立主张。仓库内 dotnet-contribution 插件的真实 C# 资产(如 repository-template.cs、service-template.cs)在接口命名、异步方法后缀、下划线私有字段、构造函数注入、日志与空值处理等方面与本模板高度一致,说明这些约定有可对照的实际工程样板。
命名约定(Naming Conventions)
通用规则
C# 命名首要遵循每种作用域使用唯一大小写风格的原则,让阅读者仅凭大小写即可判断标识符的用途:
// PascalCase for public members, types, namespaces public class UserService { } public void ProcessOrder() { } public string FirstName { get; set; } // camelCase for private fields, parameters, locals private readonly ILogger _logger; private int _itemCount; public void DoWork(string inputValue) { } // Prefix interfaces with I public interface IUserRepository { } public interface INotificationService { } // Suffix async methods with Async public async Task<User> GetUserAsync(int id) { } public async Task ProcessOrderAsync(Order order) { } // Constants: PascalCase (not SCREAMING_CASE) public const int MaxRetryCount = 3; public const string DefaultCurrency = "USD";逐条拆解:
- PascalCase用于公共成员、类型与命名空间;
- camelCase用于私有字段(可叠加下划线前缀,见下文)、方法参数与局部变量;
- 接口前缀 I是 .NET 框架惯例,不应省略;
- 异步方法以
Async结尾。在 repository-template.cs 中可以看到该约定被严格执行——GetByIdAsync、SearchAsync、CreateAsync、UpdateAsync、DeleteAsync全部遵循此命名; - 常量用 PascalCase,不用
SCREAMING_CASE,这是 .NET 命名指南与多数 C/C++/Python 习惯的关键差异,跨语言协作时尤其容易踩坑。
字段与属性命名
类型内部需要区分「可变状态存储」与「对外暴露契约」:
public class Order { // Private fields: underscore prefix + camelCase private readonly IOrderRepository _repository; private int _itemCount; // Public properties: PascalCase public int Id { get; set; } public string CustomerName { get; set; } public DateTime CreatedAt { get; init; } // Boolean properties: Is/Has/Can prefix public bool IsActive { get; set; } public bool HasDiscount { get; set; } public bool CanEdit { get; } }- 私有字段使用
_下划线前缀 + camelCase(_repository、_itemCount),readonly字段应显式标注; - 公共属性使用 PascalCase;
- 布尔属性使用
Is/Has/Can动词前缀,让条件判断读起来接近自然语言; - 只读语义用
init访问器(初始化后不可变)或仅get。
该规则在仓库 C# 资产中同样有印证:EfCoreProductRepository与DapperProductRepository都以_context、_connection、_logger这类下划线 camelCase 私有字段存储注入依赖。
异步/等待模式(Async/Await Patterns)
基础用法
C# 中所有 I/O 型操作都应优先采用async/await,而不是同步阻塞或“伪异步”写法:
// Always use async/await for I/O operations public async Task<User> GetUserAsync(int id) { var user = await _repository.FindAsync(id); if (user == null) { throw new NotFoundException($"User {id} not found"); } return user; } // Don't block on async code // Bad var user = GetUserAsync(id).Result; // Good var user = await GetUserAsync(id);反模式:.Result、.Wait()同步阻塞异步任务会引发线程池饥饿(尤其在高并发 Web 场景)并可能造成死锁(在带有 SynchronizationContext 的环境中尤甚)。任何同步阻塞替代方案都应被认定为需要整改的坏味道。
异步最佳实践
- 库代码中使用
ConfigureAwait(false),避免向调用方上下文回投递,从而降低死锁风险并提升吞吐:
public async Task<Data> FetchDataAsync() { var response = await _httpClient.GetAsync(url) .ConfigureAwait(false); return await response.Content.ReadAsAsync<Data>() .ConfigureAwait(false); }- 避免
async void,其异常无法被调用方捕获,会直接传播到线程池或应用崩溃。除 UI/框架事件处理器外一律返回Task:
// Bad public async void ProcessOrder() { } // Good public async Task ProcessOrderAsync() { } // Event handler exception private async void Button_Click(object sender, EventArgs e) { try { await ProcessOrderAsync(); } catch (Exception ex) { HandleError(ex); } }事件处理器若必须使用async void,则必须在方法体内用try/catch包裹全部逻辑,防止未观察异常击穿应用。
并行异步操作
独立操作应并行执行,用Task.WhenAll汇聚结果;响应式并发控制用SemaphoreSlim做节流:
// Execute independent operations in parallel public async Task<DashboardData> LoadDashboardAsync() { var usersTask = _userService.GetActiveUsersAsync(); var ordersTask = _orderService.GetRecentOrdersAsync(); var statsTask = _statsService.GetDailyStatsAsync(); await Task.WhenAll(usersTask, ordersTask, statsTask); return new DashboardData { Users = await usersTask, Orders = await ordersTask, Stats = await statsTask }; } // Use SemaphoreSlim for throttling public async Task ProcessItemsAsync(IEnumerable<Item> items) { using var semaphore = new SemaphoreSlim(10); // Max 10 concurrent var tasks = items.Select(async item => { await semaphore.WaitAsync(); try { await ProcessItemAsync(item); } finally { semaphore.Release(); } }); await Task.WhenAll(tasks); }要点:
- 先启动全部 Task,再统一
WhenAll,避免串行等待放大延迟;await之后再次读取await tasksTask获取结果值; SemaphoreSlim并发数(示例为 10)需按下游限流能力调整;Release()放在finally中确保异常路径也能归还信号量;- 传入
CancellationToken(如 repository-template.cs 中每个方法末尾的CancellationToken ct = default)是生产代码的另一项硬要求,便于优雅取消。
LINQ
查询语法与方法语法
- 简单查询优先方法语法(链式、可读性强);
- 含 join / group 的复杂查询可用查询语法表达意图更紧凑:
// Method syntax (preferred for simple queries) var activeUsers = users .Where(u => u.IsActive) .OrderBy(u => u.Name) .ToList(); // Query syntax (for complex queries with joins) var orderSummary = from order in orders join customer in customers on order.CustomerId equals customer.Id where order.Total > 100 group order by customer.Name into g select new { Customer = g.Key, Total = g.Sum(o => o.Total) };LINQ 最佳实践
- 用语义匹配的方法,而不是“凑出来”的等价物:
var hasItems = items.Any(); // Not: items.Count() > 0 var firstOrDefault = items.FirstOrDefault(); // Not: items.First() var count = items.Count; // Property, not Count()Count()对已实现ICollection的序列虽已优化,但对IEnumerable需要完整遍历;First()在无元素时抛异常,而FirstOrDefault()返回default。判断“是否存在元素”务必用Any()(可提前短路)而非Count() > 0;对List<T>等类型直接取.Count属性而非Count()扩展方法。
- 避免对
IEnumerable的多次枚举——每次枚举都可能重新执行底层查询(数据库往返或重复计算):
// Bad if (items.Any()) { foreach (var item in items) { } } // Good var itemList = items.ToList(); if (itemList.Count > 0) { foreach (var item in itemList) { } }- 尽早投影、只取需要的列,减少内存占用与传输量(在 EF Core / Dapper 场景下对应“只 SELECT 必要列”):
var names = users .Where(u => u.IsActive) .Select(u => u.Name) // Select only what you need .ToList();常用 LINQ 操作速查
// Filtering var adults = people.Where(p => p.Age >= 18); // Transformation var names = people.Select(p => $"{p.FirstName} {p.LastName}"); // Aggregation var total = orders.Sum(o => o.Amount); var average = scores.Average(); var max = values.Max(); // Grouping var byDepartment = employees .GroupBy(e => e.Department) .Select(g => new { Department = g.Key, Count = g.Count() }); // Joining var result = orders .Join(customers, o => o.CustomerId, c => c.Id, (o, c) => new { Order = o, Customer = c }); // Flattening var allOrders = customers.SelectMany(c => c.Orders);注意Sum(o => o.Amount)对decimal?等可空类型返回可空结果,使用前需留意;SelectMany用于“把集合的集合拍平”,是避免嵌套循环遍历的首选表达。
依赖注入(Dependency Injection)
服务注册与生命周期
ASP.NET Core 中所有协作依赖都应在Program.cs或Startup.ConfigureServices集中注册,并按真实生命周期选择容器行为:
public void ConfigureServices(IServiceCollection services) { // Transient: new instance each time services.AddTransient<IEmailService, EmailService>(); // Scoped: one instance per request services.AddScoped<IUserRepository, UserRepository>(); // Singleton: one instance for app lifetime services.AddSingleton<ICacheService, MemoryCacheService>(); // Factory registration services.AddScoped<IDbConnection>(sp => { var config = sp.GetRequiredService<IConfiguration>(); return new SqlConnection(config.GetConnectionString("Default")); }); }三类生命周期的选择要点:
- Transient:每次解析都是新实例,适合无状态轻量服务;
- Scoped:每个请求/作用域一个实例,是
DbContext、仓储类的常见归属; - Singleton:进程级单例,适合无状态缓存、配置类服务;绝不可把 Scoped/Singleton 依赖反向注入到更长生命周期的服务中(会形成“捕获依赖”陷阱);
- 工厂注册:当类型构造需要运行时参数(如按配置动态构造
SqlConnection)时,通过解析器回调完成装配。
构造器注入
依赖通过构造函数显式声明,字段全部readonly,并用空值守卫在入口即失败:
public class OrderService : IOrderService { private readonly IOrderRepository _repository; private readonly ILogger<OrderService> _logger; private readonly IEmailService _emailService; public OrderService( IOrderRepository repository, ILogger<OrderService> logger, IEmailService emailService) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _emailService = emailService ?? throw new ArgumentNullException(nameof(emailService)); } public async Task<Order> CreateOrderAsync(OrderRequest request) { _logger.LogInformation("Creating order for customer {CustomerId}", request.CustomerId); var order = new Order(request); await _repository.SaveAsync(order); await _emailService.SendOrderConfirmationAsync(order); return order; } }实践要点:
- 每个依赖一个构造函数参数,直接赋值给
readonly字段; - 构造器内用
?? throw new ArgumentNullException(nameof(...))做守卫,保证对象在不可用状态下根本不会被创建; - 使用结构化日志占位符
{CustomerId}而非字符串拼接,便于日志系统索引检索。
这一“多参数构造器 + 下划线 readonly 字段 + 空值守卫”的结构正是 repository-template.cs 中DapperProductRepository与EfCoreProductRepository两个真实实现的标准形态。
Options 模式
强类型配置优先使用IOptions<T>而非到处读IConfiguration字符串键:
// Configuration class public class EmailSettings { public string SmtpServer { get; set; } public int Port { get; set; } public string FromAddress { get; set; } } // Registration services.Configure<EmailSettings>( configuration.GetSection("Email")); // Usage public class EmailService { private readonly EmailSettings _settings; public EmailService(IOptions<EmailSettings> options) { _settings = options.Value; } }对应 appsettings.json 中同名 section("Email": { "SmtpServer": ..., "Port": ..., "FromAddress": ... })即可完成绑定;配合[Required]、[Range]等数据注解做启动期校验,是生产级配置的推荐做法。
测试(Testing)
xUnit 基础:Fact 与 Theory
[Fact]表示单条确定性测试;[Theory]+[InlineData]用多组数据复用同一测试逻辑;- 方法命名采用
方法_场景_期望结果(Add_TwoPositiveNumbers_ReturnsSum),与 general 风格指南中 “Describe behavior, not implementation” 一脉相承:
public class CalculatorTests { [Fact] public void Add_TwoPositiveNumbers_ReturnsSum() { // Arrange var calculator = new Calculator(); // Act var result = calculator.Add(2, 3); // Assert Assert.Equal(5, result); } [Theory] [InlineData(1, 1, 2)] [InlineData(0, 0, 0)] [InlineData(-1, 1, 0)] public void Add_VariousNumbers_ReturnsCorrectSum(int a, int b, int expected) { var calculator = new Calculator(); Assert.Equal(expected, calculator.Add(a, b)); } }用 Moq 做行为驱动 Mock
单元测试只应验证被测服务自身的编排逻辑,外部依赖用Mock<T>替身:
public class OrderServiceTests { private readonly Mock<IOrderRepository> _mockRepository; private readonly Mock<ILogger<OrderService>> _mockLogger; private readonly OrderService _service; public OrderServiceTests() { _mockRepository = new Mock<IOrderRepository>(); _mockLogger = new Mock<ILogger<OrderService>>(); _service = new OrderService(_mockRepository.Object, _mockLogger.Object); } [Fact] public async Task GetOrderAsync_ExistingOrder_ReturnsOrder() { // Arrange var expectedOrder = new Order { Id = 1, Total = 100m }; _mockRepository .Setup(r => r.FindAsync(1)) .ReturnsAsync(expectedOrder); // Act var result = await _service.GetOrderAsync(1); // Assert Assert.Equal(expectedOrder.Id, result.Id); _mockRepository.Verify(r => r.FindAsync(1), Times.Once); } [Fact] public async Task GetOrderAsync_NonExistingOrder_ThrowsNotFoundException() { // Arrange _mockRepository .Setup(r => r.FindAsync(999)) .ReturnsAsync((Order)null); // Act & Assert await Assert.ThrowsAsync<NotFoundException>( () => _service.GetOrderAsync(999)); } }核心手法:
- 构造器中准备好全部 mock 与被测实例(
_service),减少每个用例的样板代码; Setup(...).ReturnsAsync(...)定义行为,Verify(..., Times.Once)断言交互次数,从而把“结果正确”和“调用关系正确”都纳入测试;- 异步异常场景用
Assert.ThrowsAsync<T>断言,同时ReturnsAsync((Order)null)模拟“查无此人”。
集成测试:WebApplicationFactory
端到端验证 HTTP 契约(状态码、Content-Type、路由)时,用WebApplicationFactory<Program>直接启动内存中的宿主:
public class ApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>> { private readonly HttpClient _client; public ApiIntegrationTests(WebApplicationFactory<Program> factory) { _client = factory.CreateClient(); } [Fact] public async Task GetUsers_ReturnsSuccessAndCorrectContentType() { // Act var response = await _client.GetAsync("/api/users"); // Assert response.EnsureSuccessStatusCode(); Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString()); } }IClassFixture<WebApplicationFactory<Program>>让整个测试类共享同一测试服务器,CreateClient()返回可直接发请求的HttpClient;如需替换真实外部依赖,可覆写工厂的ConfigureWebHost/ConfigureTestServices注入 mock。这与 dotnet-contribution 中列出的测试技术栈(xUnit + Moq +WebApplicationFactory)完全对应。
常见模式(Common Patterns)
空值处理
现代 C#(启用可空引用类型后)应组合使用空条件、空合并与模式匹配:
// Null-conditional operators var length = customer?.Address?.Street?.Length; var name = user?.Name ?? "Unknown"; // Null-coalescing assignment list ??= new List<Item>(); // Pattern matching for null checks if (user is not null) { ProcessUser(user); } // Guard clauses public void ProcessOrder(Order order) { ArgumentNullException.ThrowIfNull(order); if (order.Items.Count == 0) { throw new ArgumentException("Order must have items", nameof(order)); } // Process... }?.让链式成员访问在任一环节为空时安全短路;??提供默认值;??=惰性初始化;is not null是比!= null更受推荐的空值模式匹配写法(对重载了==的类型也安全);- 参数守卫优先用 .NET 6+ 的
ArgumentNullException.ThrowIfNull(order)一行式 API。
Record 与 Init-Only 属性
不可变数据建模优先用record,配合with表达式获得“复制并修改”语义:
// Record for immutable data public record User(int Id, string Name, string Email); // Record with additional members public record Order { public int Id { get; init; } public string CustomerName { get; init; } public decimal Total { get; init; } public bool IsHighValue => Total > 1000; } // Record mutation via with expression var updatedUser = user with { Name = "New Name" };record自动提供基于值的相等性比较与ToString,天然适合 DTO、领域值对象;init属性在对象初始化后不再可变,共同构建“一经创建即不可变”的数据流,便于并发安全与缓存。
模式匹配(switch 表达式)
用表达式形式的switch代替冗长if/else与强制转换:
// Type patterns public decimal CalculateDiscount(object customer) => customer switch { PremiumCustomer p => p.PurchaseTotal * 0.2m, RegularCustomer r when r.YearsActive > 5 => r.PurchaseTotal * 0.1m, RegularCustomer r => r.PurchaseTotal * 0.05m, null => 0m, _ => throw new ArgumentException("Unknown customer type") }; // Property patterns public string GetShippingOption(Order order) => order switch { { Total: > 100, IsPriority: true } => "Express", { Total: > 100 } => "Standard", { IsPriority: true } => "Priority", _ => "Economy" }; // List patterns (C# 11) public bool IsValidSequence(int[] numbers) => numbers switch { [1, 2, 3] => true, [1, .., 3] => true, [_, _, ..] => numbers.Length >= 2, _ => false };- 类型模式:
PremiumCustomer p、RegularCustomer r when ...依次匹配并解包,when提供额外条件; - 属性模式:
{ Total: > 100, IsPriority: true }直接对属性值做关系与布尔匹配,代码密集且易读; - 列表模式(C# 11):
[1, .., 3]中的..(slice pattern)匹配任意长度的中间段,适合数组/序列的结构化校验。
Disposable 模式
非托管资源必须实现IDisposable,使用处用using(或using var范围式声明)保证及时释放:
public class ResourceManager : IDisposable { private bool _disposed; private readonly FileStream _stream; public ResourceManager(string path) { _stream = File.OpenRead(path); } public void DoWork() { ObjectDisposedException.ThrowIf(_disposed, this); // Work with _stream } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _stream?.Dispose(); } _disposed = true; } } // Using statement using var manager = new ResourceManager("file.txt"); manager.DoWork();规范要点:
- 公开的
Dispose()调用受保护的Dispose(bool)虚方法,供派生类扩展清理逻辑; - 置
_disposed = true并调用GC.SuppressFinalize(this),避免对象被不必要地送入终结队列; - 使用方始终通过
using var声明,方法作用域结束时自动调用Dispose(),任何已释放对象上的操作由ObjectDisposedException.ThrowIf兜底拦截。
代码组织(Code Organization)
文件内成员排列
“一类型一文件”是通用原则;单个类型内部成员按固定优先级从上到下排列,形成稳定的阅读顺序:
public class UserService { // 1. Constants private const int MaxRetries = 3; // 2. Static fields private static readonly object _lock = new(); // 3. Instance fields private readonly IUserRepository _repository; // 4. Constructors public UserService(IUserRepository repository) { _repository = repository; } // 5. Properties public int TotalUsers { get; private set; } // 6. Public methods public async Task<User> GetUserAsync(int id) { } // 7. Private methods private void ValidateUser(User user) { } }(说明:文件末尾以#region分组多实现时,如 repository-template.cs 中按 “Dapper Implementation / EF Core Implementation / DbContext Configuration / Advanced Patterns / Entity Definitions” 组织,同样遵循“从外部契约到内部细节”的顺序思想。)
解决方案级项目结构
跨项目的分层结构建议按职责拆分 src/tests,避免循环依赖与“万能类库”:
Solution/ ├── src/ │ ├── MyApp.Api/ # Web API project │ ├── MyApp.Core/ # Domain/business logic │ ├── MyApp.Infrastructure/ # Data access, external services │ └── MyApp.Shared/ # Shared utilities ├── tests/ │ ├── MyApp.UnitTests/ │ └── MyApp.IntegrationTests/ └── MyApp.sln这套布局与前述各层约定协同工作:
- MyApp.Api:只承载控制器/中间件/DI 装配,不包含业务规则;
- MyApp.Core:领域实体、领域服务与接口定义(对
I前缀接口 + record + 模式匹配的天然归宿); - MyApp.Infrastructure:EF Core/Dapper 实现与外部服务适配器(
Async后缀与ConfigureAwait(false)的高频区); - MyApp.Shared:跨层工具,保持低耦合、高内聚;
- tests与src平行,单元测试与集成测试(
WebApplicationFactory)目录隔离,构建产物互不污染。
结合 Conductor 工作流落地本规范
要把以上规范真正变成团队的协作基线,推荐路径如下:
- 运行
/conductor:setup(见 setup.md),在 Code Style Guides 问答中选择生成 C# 指南;生成的conductor/code_styleguides/csharp.md即本模板的副本; - 在 index.md 的导航表中确认 C# 指南被登记,方便 AI 与成员随时查阅;
- 建立新 track(
/conductor:new-track)并在spec.md中声明“遵循code_styleguides/csharp.md”,随后/conductor:implement阶段的生成代码即会被这些约定持续约束; - 与 dotnet-contribution 插件配合使用,其仓库类模板、服务类模板与 EF Core/Dapper 参考文档可作为规范的可运行范例,让抽象约定落到可复制代码;
- 针对团队已有代码,还可参考 general.md 中 “Code Review Checklist” 的检查项(可读性、边界、错误处理、安全、测试覆盖、约定一致性)逐项验收。
小结
本 C# 风格指南覆盖了从标识符命名、异步并发、LINQ 到依赖注入、测试与工程结构的一整套 .NET 开发约定。它既是 Conductor 初始化流程生成的“项目宪法”,也可独立作为团队 C# 编码评审的核对清单。实际使用时建议:
- 命名即文档,让 PascalCase/camelCase/
I前缀/Async后缀自动传达代码意图; - async 全链路化,绝不
.Result/.Wait(),库代码ConfigureAwait(false),事件处理器例外必须 try/catch; - LINQ 语义优先、避免重复枚举、尽早投影;
- DI 按生命周期选型、构造器注入 + 空值守卫、
IOptions<T>管理配置; - 用 xUnit + Moq +
WebApplicationFactory建立“单元到契约”的测试纵深; - 代码组织遵循“一类型一文件、成员有序、src/tests 分层”的工程化结构。
将本模板复制进你的项目conductor/code_styleguides/,即可让 AI 编码助手与人类开发者共用同一套 .NET 语言规范,从源头减少评审分歧与返工。
【免费下载链接】agentsMulti-harness agentic plugin marketplace for Claude Code, Codex, Cursor, OpenCode, GitHub Copilot, and Google Antigravity项目地址: https://gitcode.com/GitHub_Trending/agents24/agents
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考