问题现象:AI生成的C#代码总是不对味

用Copilot或Cursor写过C#的朋友,大多遇到过这类情况:

  • 类名用了小驼峰(customerRepo),而.NET标准要求帕斯卡命名(CustomerRepo
  • 异步方法没有 Async 后缀,或者用了 .Result 阻塞
  • 没有依赖注入意识,构造函数里 new 一堆服务
  • 日志不用 ILogger<T>,而是直接 Console.WriteLine

AI不傻,只是它缺少一份 “当前上下文应该遵循什么规范” 的说明。默认情况下,大模型会按平均训练数据来生成——但C#的开源训练数据中风格非常杂,有Unity的、有Xamarin的、有古老WinForms的。而你要的是现代ASP.NET Core风格。

微软最近开源的 dotnet/skills 就是来解决这个问题的:提供一组预定义的“技能”文件,告诉AI代理特定的编码规则。今天我就拆解它的核心思路,并给你一个可以直接复用的模板。

上下文结构分析:Skill 文件里到底写了什么

我翻看了仓库里的几个Skill文件(例如 csharp-conventions.md),其设计模式非常清晰:

  1. 角色定义You are a senior .NET developer
  2. 全局规则:明确的命名、异步、DI、日志规范
  3. 负面清单Do NOT use .Result.
  4. 示例偏好Prefer switch expressions over if-else when pattern matching

这种结构的本质是 压缩上下文:把分散在数千篇文档中的最佳实践浓缩成半页规则。模型读一次就能记住并执行。

csharp code conventions comparison
左:无Skill生成的代码,右:使用Skill后的代码。注意命名、异步标注、日志注入的差异。

优化方案:一份可直接复用的C#编码规范Prompt

下面是我从dotnet/skills中提取并优化的模板,你可以直接粘贴到GitHub Copilot的Custom Instructions或Cursor的Rules中。

text
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
You are a senior .NET developer, generating C# code for modern .NET 8+ projects.
Follow these rules strictly:

1. Naming:
   - PascalCase for class names, public methods, properties, events, constants, and record names.
   - camelCase for local variables, method parameters, and private fields (no underscore prefix unless for backing fields).
   - Use `_camelCase` for private fields only when they are backing fields of auto-properties.

2. Async:
   - Every async method must end with `Async` suffix.
   - Use `await` instead of `.Result` or `.Wait()`. Never use `Task.WaitAll` or `Task.WhenAny` in application code.
   - Expose `CancellationToken` parameter in all long-running operations.

3. Dependency Injection:
   - Register services via constructor injection. Do NOT use `new` to instantiate services that have dependencies.
   - Use `ILogger<T>` for logging. Do NOT use `Console.WriteLine` or `Debug.WriteLine`. Prefer structured logging.

4. Nullable Reference Types:
   - Enable `<Nullable>enable</Nullable>`.
   - Use `string?` for nullable strings, `string` for non-nullable.
   - Avoid `!.` (null-forgiving operator) unless you are 100% sure.

5. Collections & Interfaces:
   - Accept `IEnumerable<T>` for read-only input, `IReadOnlyCollection<T>` if count matters, `IList<T>` if indexed access is needed.
   - Return `IReadOnlyList<T>` or `IEnumerable<T>` from public methods; avoid exposing `List<T>` or arrays directly.

6. Exceptions:
   - Do NOT catch generic `Exception` unless you rethrow or log and recover in a specific way.
   - Prefer custom exception classes derived from `Exception` for domain errors.

7. Code Style:
   - Use `var` only when the type is obvious (e.g., `var list = new List<int>()`). Otherwise write explicit types.
   - Prefer primary constructors for simple DTOs/records.
   - Use file-scoped namespaces (`namespace Foo.Bar;`).

为什么这样写有效?

大模型的注意力会分散到上下文的每个角落。如果你只在对话中偶尔说一句“用PascalCase”,模型可能在关键决策时忘记。但如果你把规则列表放在靠前的位置(作为系统提示),模型会把它当作“任务说明书”,每次生成时都会参考。规则越具体、越像代码格式,模型执行准确性越高。

实验对比:差Prompt vs 好Prompt

我让同一个模型(GPT-4o)分别用两种Prompt生成同一个功能:一个异步日志记录类,供控制器使用。

差Prompt: 写一个C#日志记录器,在程序启动时写一条消息

csharp
1 2 3 4 5
public class Logger {
    public void Log(string message) {
        Console.WriteLine(message);
    }
}

问题:同步、无DI、无命名规范、无结构化。

好Prompt(带上上面的Skill模板): 生成结果:

csharp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public sealed class LoggingService : ILoggingService
{
    private readonly ILogger<LoggingService> _logger;

    public LoggingService(ILogger<LoggingService> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task LogStartupMessageAsync(string message, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Application started: {Message}", message);
        await Task.CompletedTask; // Placeholder for any async work
    }
}

明显差异:异步后缀、DI注入、ILogger、CancellationToken、命名合规。

csharp code generation comparison table
两个Prompt生成的代码在命名、异步、DI上的评分对比。好Prompt在每项上都达到满分。

适用场景和边界

这个模板适合以下场景:

  • 编写新的API、服务、控制器、DTO、仓储层
  • 团队希望统一代码风格,但还没时间写StyleCop规则
  • 使用AI编码助手进行初期骨架生成

边界:

  • 规则过多(超过20条)会导致模型忽略部分规则,建议精简到10条以内
  • 对于底层性能敏感代码(如Span、内存池),这些规则可能需要放宽(例如允许使用unsafe)
  • 旧项目(.NET Framework 4.x)不适用异步后缀等规则,需调整

扩展用法:2-3个变体

  1. EF Core Entity Skill:在规则中加入 Use data annotations only when necessary; prefer Fluent API in OnModelCreatingAdd a private constructor for DbContext in migrations 等。
  2. ASP.NET Core Minimal API Skill:强调 Use MapGetnotapp.UseEndpoints; Add [AsParameters] for complex queries
  3. Blazor Component SkillUse @codenot code-behind; Use[Parameter]and[SupplyParameterFromQuery]; Avoid StateHasChanged in loops

你完全可以基于这个模式,为你团队的项目定制一份Skill文件。dotnet/skills 最大的价值不是那几百个文件,而是它教了我们一种 “如何让AI理解你的代码规范” 的方法。

个人观点

我特别推荐你把这份Prompt放在C#项目的 .github/copilot-instructions.md 里(GitHub Copilot会读取),或者Cursor的 .cursorrules 中。这样整个团队在使用AI时,生成的代码会自动对齐到同一套标准,减少代码审查时因风格差异引发的无效讨论。

如果你想要更弹性的控制,还可以在规则清单最后加一句:If the code is for a legacy project, ask me before applying modern conventions. —— 这样AI会在关键决策前主动征求意见,而不是盲目执行。

这才是dotnet/skills背后真正的工程师思维:不是等着AI完美,而是用结构化上下文主动塑造AI的行为。