第 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.json2. 版本管理
语义化版本(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 LocalTestQ: 如何处理破坏性变更?
A:
- 增加主版本号(例如 1.x.x → 2.0.0)
- 在 CHANGELOG.md 中明确说明
- 提供迁移指南
- 考虑保留旧 API 并标记为 Obsolete
Q: 如何监控生成器性能?
A:
- 使用
Stopwatch测量执行时间 - 在 DEBUG 模式下输出诊断信息
- 使用 BenchmarkDotNet 进行基准测试
- 监控用户反馈的编译时间
🎉 恭喜完成!
你已经完成了所有 8 个学习步骤!现在你已经掌握了:
- ✅ 源生成器的基本概念和工作原理
- ✅ 创建和使用源生成器
- ✅ 实用的 ToString 生成器示例
- ✅ 性能优化和增量生成器
- ✅ 进阶主题(Builder 模式、诊断报告)
- ✅ 测试和调试方法
- ✅ 生产环境最佳实践