Skip to content

第 8 步:生产实践

加载进度中...

📋 本步目标

  • 学习如何打包和发布源生成器
  • 掌握版本管理和兼容性策略
  • 了解性能监控和优化
  • 学习文档编写最佳实践
  • 处理实际生产环境中的问题

⏱️ 预计时间

约 2-3 小时


📚 学习内容

1. 打包为 NuGet 包

配置项目文件

编辑生成器项目的 .csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>latest</LangVersion>
    
    <!-- NuGet 包信息 -->
    <PackageId>YourCompany.SourceGenerators.ToString</PackageId>
    <Version>1.0.0</Version>
    <Authors>Your Name</Authors>
    <Company>Your Company</Company>
    <Description>自动生成 ToString 方法的源生成器</Description>
    <PackageTags>source-generator;roslyn;toString</PackageTags>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <PackageProjectUrl>https://github.com/yourname/tostring-generator</PackageProjectUrl>
    <RepositoryUrl>https://github.com/yourname/tostring-generator</RepositoryUrl>
    
    <!-- 重要:将生成器打包为分析器 -->
    <IncludeBuildOutput>false</IncludeBuildOutput>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
  </ItemGroup>

  <!-- 将生成器 DLL 打包到 analyzers 目录 -->
  <ItemGroup>
    <None Include="$(OutputPath)\$(AssemblyName).dll" 
          Pack="true" 
          PackagePath="analyzers/dotnet/cs" 
          Visible="false" />
  </ItemGroup>

</Project>

关键配置说明

IncludeBuildOutput="false"

  • 不将 DLL 作为普通引用包含
  • 生成器不是运行时依赖

PackagePath="analyzers/dotnet/cs"

  • NuGet 包的特殊路径
  • 告诉 NuGet 这是一个 C# 分析器/生成器
  • 编译器会自动加载这个路径下的 DLL

创建 README.md

在项目根目录创建 README.md

markdown
# ToString Generator

自动为类生成 ToString 方法的源生成器。

## 安装

```bash
dotnet add package YourCompany.SourceGenerators.ToString

使用

csharp
using YourCompany.SourceGenerators;

[GenerateToString]
public partial class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// 自动生成 ToString 方法
var person = new Person { Name = "Alice", Age = 30 };
Console.WriteLine(person);
// 输出: Person { Name = Alice, Age = 30 }

要求

  • .NET 6.0 或更高版本
  • C# 9.0 或更高版本(支持 partial 类)

许可证

MIT


#### 打包和发布

```bash
# 清理之前的构建
dotnet clean

# 打包(会自动构建)
dotnet pack --configuration Release

# 生成的包在 bin/Release/ 目录
# 例如:YourCompany.SourceGenerators.ToString.1.0.0.nupkg

# 本地测试包
dotnet nuget add source ./bin/Release --name LocalTest

# 发布到 NuGet.org
dotnet nuget push bin/Release/*.nupkg --api-key YOUR_API_KEY --source https://api.nuget.org/v3/index.json

2. 版本管理

语义化版本(SemVer)

使用 主版本.次版本.修订版本 格式:

1.0.0 → 1.0.1 → 1.1.0 → 2.0.0

版本号规则

版本类型何时增加示例
主版本不兼容的 API 变更重命名特性、删除功能
次版本向后兼容的新功能添加新特性、新选项
修订版本向后兼容的 bug 修复修复生成错误、性能优化

变更日志(CHANGELOG.md)

markdown
# Changelog

## [1.1.0] - 2024-01-15

### Added
- 添加 `[ToStringIgnore]` 特性支持
- 支持忽略特定属性

### Fixed
- 修复 null 值处理问题

## [1.0.1] - 2024-01-10

### Fixed
- 修复多个类在同一文件中的问题

## [1.0.0] - 2024-01-01

### Added
- 初始版本
- 基本 ToString 生成功能

3. 兼容性策略

支持多个 .NET 版本

生成器使用 netstandard2.0,但生成的代码需要适配不同版本:

csharp
private static string GenerateCode(ClassInfo classInfo)
{
    var sb = new StringBuilder();
    
    sb.AppendLine("#nullable enable");
    sb.AppendLine("using System;");
    sb.AppendLine();
    
    sb.AppendLine($"namespace {classInfo.Namespace}");
    sb.AppendLine("{");
    sb.AppendLine($"    partial class {classInfo.Name}");
    sb.AppendLine("    {");
    
    // 使用条件编译支持不同版本
    sb.AppendLine("#if NET6_0_OR_GREATER");
    sb.AppendLine("        public override string ToString()");
    sb.AppendLine("        {");
    sb.AppendLine("            return $\"{GetType().Name} {{ ... }}\";");
    sb.AppendLine("        }");
    sb.AppendLine("#else");
    sb.AppendLine("        public override string ToString()");
    sb.AppendLine("        {");
    sb.AppendLine("            return string.Format(\"{0} {{ ... }}\", GetType().Name);");
    sb.AppendLine("        }");
    sb.AppendLine("#endif");
    
    sb.AppendLine("    }");
    sb.AppendLine("}");
    sb.AppendLine("#nullable restore");
    
    return sb.ToString();
}

4. 性能监控

添加性能诊断

csharp
using System.Diagnostics;

[Generator]
public class ToStringGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterSourceOutput(
            classDeclarations,
            (spc, classInfo) =>
            {
                var sw = Stopwatch.StartNew();
                GenerateToStringMethod(spc, classInfo);
                sw.Stop();
                
                // 在 DEBUG 模式下报告性能
                #if DEBUG
                if (sw.ElapsedMilliseconds > 100)
                {
                    var descriptor = new DiagnosticDescriptor(
                        "TSG999",
                        "Performance Warning",
                        $"Generating ToString for {classInfo.Name} took {sw.ElapsedMilliseconds}ms",
                        "Performance",
                        DiagnosticSeverity.Warning,
                        isEnabledByDefault: true);
                    
                    spc.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None));
                }
                #endif
            });
    }
}

5. 文档最佳实践

XML 文档注释

csharp
/// <summary>
/// 标记类以自动生成 ToString 方法。
/// </summary>
/// <remarks>
/// 使用此特性的类必须标记为 partial。
/// 生成的 ToString 方法会包含所有公共属性。
/// </remarks>
/// <example>
/// <code>
/// [GenerateToString]
/// public partial class Person
/// {
///     public string Name { get; set; }
///     public int Age { get; set; }
/// }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class GenerateToStringAttribute : Attribute
{
}

创建故障排除文档

创建 TROUBLESHOOTING.md

markdown
# 故障排除

## 生成器没有运行

**症状**:没有生成任何代码。

**检查清单**
- [ ] 类是否标记了 `[GenerateToString]`
- [ ] 类是否使用了 `partial` 关键字
- [ ] 项目引用配置是否正确
- [ ] 是否重新构建了项目

**解决方案**
1. 清理并重新构建:`dotnet clean && dotnet build`
2. 检查生成的文件:`obj/Debug/net8.0/generated/`
3. 启用详细日志:`dotnet build -v detailed`

## 编译错误

**症状**:使用者项目编译失败。

**常见原因**
- 生成的代码有语法错误
- 命名空间不匹配
- 缺少必要的 using 语句

**解决方案**
1. 查看生成的文件内容
2. 检查错误消息
3. 启用 `EmitCompilerGeneratedFiles` 查看生成的代码

6. 错误处理

优雅的错误报告

csharp
private static ClassInfo? GetClassInfo(GeneratorSyntaxContext context)
{
    try
    {
        var classDecl = (ClassDeclarationSyntax)context.Node;
        var symbol = context.SemanticModel.GetDeclaredSymbol(classDecl);
        
        if (symbol == null)
        {
            ReportDiagnostic(context, "TSG001", 
                "无法获取类的符号信息", 
                classDecl.GetLocation());
            return null;
        }
        
        // 验证类是 partial
        if (!classDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
        {
            ReportDiagnostic(context, "TSG002",
                $"类 {symbol.Name} 必须标记为 partial",
                classDecl.Identifier.GetLocation());
            return null;
        }
        
        return new ClassInfo(symbol.Name, symbol.ContainingNamespace.ToDisplayString());
    }
    catch (Exception ex)
    {
        // 记录异常但不中断编译
        ReportDiagnostic(context, "TSG999",
            $"处理类时发生错误: {ex.Message}",
            Location.None);
        return null;
    }
}

private static void ReportDiagnostic(
    GeneratorSyntaxContext context,
    string id,
    string message,
    Location location)
{
    var descriptor = new DiagnosticDescriptor(
        id,
        "ToString Generator",
        message,
        "ToStringGenerator",
        DiagnosticSeverity.Error,
        isEnabledByDefault: true);
    
    context.ReportDiagnostic(Diagnostic.Create(descriptor, location));
}

7. 维护最佳实践

持续集成(CI)

创建 .github/workflows/ci.yml

yaml
name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: 8.0.x
    
    - name: Restore dependencies
      run: dotnet restore
    
    - name: Build
      run: dotnet build --no-restore
    
    - name: Test
      run: dotnet test --no-build --verbosity normal
    
    - name: Pack
      run: dotnet pack --no-build --configuration Release

示例项目

在仓库中包含 samples/ 目录:

samples/
├── BasicUsage/
│   ├── Program.cs
│   └── BasicUsage.csproj
├── AdvancedUsage/
│   ├── Program.cs
│   └── AdvancedUsage.csproj
└── README.md

✅ 检查点

✅ 检查点

完成以下任务后,可以进入下一步:


💡 常见问题

Q: 如何测试 NuGet 包?

A: 使用本地 NuGet 源:

bash
# 创建本地源
dotnet nuget add source ./bin/Release --name LocalTest

# 在测试项目中安装
dotnet add package YourPackage --version 1.0.0 --source LocalTest
Q: 如何处理破坏性变更?

A:

  1. 增加主版本号(例如 1.x.x → 2.0.0)
  2. 在 CHANGELOG.md 中明确说明
  3. 提供迁移指南
  4. 考虑保留旧 API 并标记为 Obsolete
Q: 如何监控生成器性能?

A:

  1. 使用 Stopwatch 测量执行时间
  2. 在 DEBUG 模式下输出诊断信息
  3. 使用 BenchmarkDotNet 进行基准测试
  4. 监控用户反馈的编译时间

🎉 恭喜完成!

你已经完成了所有 8 个学习步骤!现在你已经掌握了:

  • ✅ 源生成器的基本概念和工作原理
  • ✅ 创建和使用源生成器
  • ✅ 实用的 ToString 生成器示例
  • ✅ 性能优化和增量生成器
  • ✅ 进阶主题(Builder 模式、诊断报告)
  • ✅ 测试和调试方法
  • ✅ 生产环境最佳实践

下一步建议

  1. 深入学习

  2. 实践项目

    • 为你的项目创建自定义生成器
    • 贡献到开源项目
    • 分享你的经验
  3. 持续改进

    • 关注 Roslyn 的新特性
    • 学习其他优秀生成器的实现
    • 参与社区讨论

⏭️ 导航

基于 MIT 许可发布