一、源生成器是什么?它能解决什么问题?
想象一下你每天都要写大量重复的样板代码,比如DTO类、API客户端或者简单的CRUD操作。每次修改实体后,都得手动同步修改相关代码,既枯燥又容易出错。这时候,源生成器(Source Generator)就像个贴心小助手,能在编译时自动帮你生成这些代码。
源生成器是Roslyn编译器的一部分,它允许我们在代码编译过程中分析现有代码并生成新的C#源代码文件。这些生成的文件会和手写代码一起参与编译,整个过程对开发者完全透明。
举个例子,假设我们有个简单的用户实体类:
// 技术栈:.NET 6 + C# 10
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
如果每次都要为这样的类手动编写DTO、验证逻辑等,工作量会很大。源生成器可以帮我们自动完成这些工作。
二、如何创建一个基础的源生成器
让我们从创建一个简单的源生成器开始,它会为标记了特定特性的类自动生成ToString()方法。
首先创建一个类库项目,需要安装必要的NuGet包:
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" />
</ItemGroup>
然后创建我们的生成器:
// 技术栈:.NET 6 + C# 10
[Generator]
public class ToStringGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// 注册一个语法接收器来监听类声明
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is not SyntaxReceiver receiver)
return;
// 获取编译中的语义模型
var compilation = context.Compilation;
foreach (var classDecl in receiver.CandidateClasses)
{
var model = compilation.GetSemanticModel(classDecl.SyntaxTree);
var typeSymbol = model.GetDeclaredSymbol(classDecl);
// 只为有[GenerateToString]特性的类生成代码
if (typeSymbol.GetAttributes().Any(ad =>
ad.AttributeClass?.Name == "GenerateToStringAttribute"))
{
string source = GenerateToStringCode(typeSymbol);
context.AddSource($"{typeSymbol.Name}_ToString.g.cs", source);
}
}
}
private string GenerateToStringCode(INamedTypeSymbol typeSymbol)
{
var properties = typeSymbol.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.IsStatic && p.DeclaredAccessibility == Accessibility.Public);
var sb = new StringBuilder();
sb.AppendLine($"// <auto-generated/>");
sb.AppendLine($"namespace {typeSymbol.ContainingNamespace.ToDisplayString()}");
sb.AppendLine("{");
sb.AppendLine($" partial class {typeSymbol.Name}");
sb.AppendLine(" {");
sb.AppendLine(" public override string ToString()");
sb.AppendLine(" {");
sb.AppendLine(" return $\"");
bool first = true;
foreach (var prop in properties)
{
if (!first) sb.Append(", ");
sb.Append($"{prop.Name}: {{{prop.Name}}}");
first = false;
}
sb.AppendLine("\";");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
}
class SyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> CandidateClasses { get; } = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax classDecl)
{
CandidateClasses.Add(classDecl);
}
}
}
使用这个生成器非常简单:
[GenerateToString]
public partial class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
// 自动生成的ToString()方法会输出:"Id: {Id}, Name: {Name}, Email: {Email}"
三、更高级的应用:自动生成API客户端
让我们看一个更实用的例子:自动生成API客户端。假设我们有一些用特性标记的接口,想为它们自动生成HttpClient调用代码。
首先定义我们的特性:
// 技术栈:.NET 6 + C# 10
[AttributeUsage(AttributeTargets.Interface)]
public class ApiClientAttribute : Attribute
{
public string BaseUrl { get; }
public ApiClientAttribute(string baseUrl)
{
BaseUrl = baseUrl;
}
}
[AttributeUsage(AttributeTargets.Method)]
public class GetAttribute : Attribute
{
public string Path { get; }
public GetAttribute(string path)
{
Path = path;
}
}
然后创建一个生成器来识别这些接口并生成客户端代码:
[Generator]
public class ApiClientGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new ApiSyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is not ApiSyntaxReceiver receiver)
return;
var compilation = context.Compilation;
foreach (var interfaceDecl in receiver.CandidateInterfaces)
{
var model = compilation.GetSemanticModel(interfaceDecl.SyntaxTree);
var interfaceSymbol = model.GetDeclaredSymbol(interfaceDecl);
var apiClientAttr = interfaceSymbol.GetAttributes()
.FirstOrDefault(ad => ad.AttributeClass?.Name == "ApiClientAttribute");
if (apiClientAttr == null)
continue;
string baseUrl = apiClientAttr.ConstructorArguments[0].Value.ToString();
string source = GenerateApiClient(interfaceSymbol, baseUrl);
context.AddSource($"{interfaceSymbol.Name}Client.g.cs", source);
}
}
private string GenerateApiClient(INamedTypeSymbol interfaceSymbol, string baseUrl)
{
var sb = new StringBuilder();
sb.AppendLine($"// <auto-generated/>");
sb.AppendLine("using System.Net.Http;");
sb.AppendLine("using System.Threading.Tasks;");
sb.AppendLine();
sb.AppendLine($"namespace {interfaceSymbol.ContainingNamespace.ToDisplayString()}");
sb.AppendLine("{");
sb.AppendLine($" public class {interfaceSymbol.Name}Client : {interfaceSymbol.ToDisplayString()}");
sb.AppendLine(" {");
sb.AppendLine(" private readonly HttpClient _httpClient;");
sb.AppendLine();
sb.AppendLine($" public {interfaceSymbol.Name}Client(HttpClient httpClient)");
sb.AppendLine(" {");
sb.AppendLine($" _httpClient = httpClient;");
sb.AppendLine($" _httpClient.BaseAddress = new System.Uri(\"{baseUrl}\");");
sb.AppendLine(" }");
sb.AppendLine();
foreach (var method in interfaceSymbol.GetMembers().OfType<IMethodSymbol>())
{
var getAttr = method.GetAttributes()
.FirstOrDefault(ad => ad.AttributeClass?.Name == "GetAttribute");
if (getAttr == null)
continue;
string path = getAttr.ConstructorArguments[0].Value.ToString();
sb.AppendLine($" public async {method.ReturnType} {method.Name}()");
sb.AppendLine(" {");
sb.AppendLine($" var response = await _httpClient.GetAsync(\"{path}\");");
sb.AppendLine($" response.EnsureSuccessStatusCode();");
if (method.ReturnType is INamedTypeSymbol returnType &&
returnType.IsGenericType &&
returnType.ConstructedFrom.SpecialType == SpecialType.System_Threading_Tasks_Task)
{
sb.AppendLine($" return await response.Content.ReadAsAsync<{returnType.TypeArguments[0]}>();");
}
else
{
sb.AppendLine(" return;");
}
sb.AppendLine(" }");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
}
使用示例:
[ApiClient("https://api.example.com")]
public interface IUserService
{
[Get("/api/users")]
Task<List<User>> GetUsers();
[Get("/api/users/{id}")]
Task<User> GetUser(int id);
}
// 自动生成的客户端可以直接使用:
var httpClient = new HttpClient();
var userService = new UserServiceClient(httpClient);
var users = await userService.GetUsers();
四、源生成器的应用场景与最佳实践
源生成器最擅长的场景包括:
- 减少样板代码:DTO、映射代码、API客户端等
- 基于约定生成代码:如Entity Framework的DbContext
- 编译时AOP:日志、缓存等横切关注点
- 性能优化:避免反射,生成直接调用的代码
优点:
- 编译时完成,不影响运行时性能
- 生成的代码可调试
- 与手写代码无缝集成
- 强类型检查
缺点:
- 学习曲线较陡
- 调试生成器本身比较困难
- 对项目结构有一定要求
注意事项:
- 确保生成器项目是netstandard2.0目标框架
- 使用部分类(partial)以便混合手写和生成代码
- 生成的代码文件名以.g.cs结尾是约定
- 考虑增量生成以提高性能
- 处理错误情况,给出明确的诊断信息
最佳实践:
- 为生成器编写单元测试
- 提供良好的诊断信息
- 考虑IDE体验,如智能感知支持
- 文档化生成器的约定和行为
五、总结与展望
源生成器是C#生态中一个强大的工具,它能显著提升开发效率,减少重复劳动。通过本文的示例,我们看到了如何创建从简单到复杂的生成器。虽然上手有一定难度,但一旦掌握,它能带来巨大的生产力提升。
未来,随着.NET生态的发展,源生成器可能会在更多场景中发挥作用,比如:
- 更智能的ORM代码生成
- 领域特定语言(DSL)支持
- 跨平台代码适配
- 更高级的编译时优化
如果你经常写重复的模式化代码,不妨尝试用源生成器自动化这个过程。刚开始可能会遇到一些挑战,但长期来看,这种投资绝对值得。
评论