.NET 开发中的 Dapr、Roslyn 与源生成器
1. Dapr 简介
Dapr 是一个重要的开发工具,它将一系列组件和原则打包成一个开发模型。以计数器应用为例,当应用停止并重新启动时,计数器不会重新从 0 开始,因为它会将状态保存在 Redis 中。Dapr 的主要优势在于,开发者只需针对 Dapr API 进行开发,其余的运行时管理工作由它自动处理。
2. .NET 架构选择
.NET 一直致力于推广良好、简洁的架构,在 .NET 6 中依然延续这一趋势。像 eShop On Containers 这样的开源参考项目,能帮助开发者和架构师为项目找到最佳架构。Dapr 等框架则有助于缓解分布式应用中管理各种构建块的困难。不过,架构选择没有一刀切的方案,开发者需要从更高、更抽象的层面审视项目,选择合适的架构,避免过度设计,保持简单。
3. .NET 编译器平台 - Roslyn
Roslyn 是 .NET 编译器平台的核心,它为开发者提供了分析代码、执行编码规范等功能。以往编译器如同黑盒,将源代码转换为中间语言,而 Roslyn 则将编译器平台开放,并提供 API 集,供开发者编写代码增强工具。
3.1 Roslyn 的功能
- 分析器和代码修复 :分析器能检查代码是否符合规范,.NET 自带了一组默认分析器。当编写不符合规范的代码时,分析器会发出通知。代码修复则为开发者提供重构代码的建议,例如将 For Each 块转换为简单的 LINQ 语句。
-
SDK 提供的 API
:Roslyn 附带的 SDK 提供了编译器 API、诊断 API、脚本 API 和工作区 API。
- 编译器 API :包含特定语言的代码编译器,如 C# 的 csc.exe,并且包含编译器管道各阶段的对象模型。
- 诊断 API :能在代码中显示“波浪线”,基于 Roslyn 分析器分析语法、赋值和语义,生成警告或错误,可被 linting 工具用于在代码审查时确保团队规范得到遵守。
- 脚本 API :可将代码片段作为脚本运行,例如用于 C# 的 Read, Evaluate, Print Loop(REPL)。
- 工作区 API :为整个解决方案的代码分析和重构提供入口点,支持 IDE 功能,如查找所有引用和格式化代码。
3.2 语法树
语法树是编译器 API 暴露的数据结构,代表代码的语法结构,具有以下特点:
- 包含开发者输入的完整代码信息,包括注释、编译器预指令和空格。
- 可以从语法树重构出原始代码,它是从原始源代码解析而来的不可变结构。
- 线程安全且不可变,是代码的状态快照,框架内的工厂方法确保请求的更改被推回到代码中,并根据源代码的最新状态生成新的语法树。
3.3 Roslyn SDK
要开发自己的 Roslyn 分析器,需要安装 Roslyn SDK,它是 Visual Studio 安装程序的可选组件。安装后,会获得以下工具:
-
语法可视化器
:在 Visual Studio 的“视图” -> “其他窗口” -> “语法可视化器”中,能展示当前打开代码文件的语法树,其位置与源文件中的光标同步。
-
项目模板
:可用于构建独立的控制台应用或 VSIX 格式的 Visual Studio 扩展,支持 C# 和 Visual Basic。
3.4 创建分析器
以下是创建独立代码分析器的步骤:
1. 代码分析工具需用 .NET Framework 编写,但支持分析 .NET 6 代码。默认模板会查询系统上可用的 MSBuild 版本,可通过调用
MSBuildLocator.QueryVisualStudioInstances().ToArray()
获取安装版本列表。
2. 清空
Main
方法,开始实现代码分析器。分析代码时,需要
SyntaxTree
对象,它保存代码文档的解析表示。以下是获取语法树和编译单元的示例代码:
static Task Main(string[] args)
{
const string code = @"using System; using System.Linq; Console.WriteLine(""Hello World"");";
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
}
-
创建继承自
CSharpSyntaxWalker的自定义类,例如UsingDirectivesWalker,用于遍历语法树并提取特定信息。
class UsingDirectivesWalker : CSharpSyntaxWalker
{
public override void VisitUsingDirective(UsingDirectiveSyntax node)
{
Console.WriteLine($"Found using {node.Name}.");
}
}
- 使用自定义语法 walker 分析代码:
static Task Main(string[] args)
{
const string code = @"using System; using System.Linq; Console.WriteLine(""Hello World"");";
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
var collector = new UsingDirectivesWalker();
collector.Visit(root);
Console.Read();
return Task.CompletedTask;
}
以下是创建分析器的流程图:
graph TD;
A[查询 MSBuild 版本] --> B[清空 Main 方法];
B --> C[获取语法树和编译单元];
C --> D[创建自定义语法 walker];
D --> E[使用自定义语法 walker 分析代码];
4. 源生成器
源生成器是编译器平台的新特性,在代码编译期间运行,能根据代码分析生成额外的代码文件,并将其包含在编译中。
4.1 源生成器的优势
源生成器用 C# 编写,可防止使用反射,在编译时生成额外类,通常能提高性能。但需要注意的是,源生成器只能生成和注入额外代码,不能修改开发者编写的代码。
4.2 编写源生成器
以下是编写源生成器的详细步骤:
1.
创建项目
:源生成器适用于 .NET 6 项目,但在编写时需定义在 .NET Standard 2.0 类库中。创建类库后,添加实现
ISourceGenerator
接口的类,需先安装
Microsoft.CodeAnalysis
NuGet 包。
public interface ISourceGenerator
{
void Initialize(GeneratorInitializationContext context);
void Execute(GeneratorExecutionContext context);
}
- 定义测试项目和属性 :创建一个 .NET 6 控制台应用作为测试项目,定义一个简单的属性用于过滤需要生成 DTO 的类。
internal class GenerateDtoAttribute : Attribute
{
}
[GenerateDto]
public class Product
{
public string Name { get; set; }
public string Description { get; set; }
public double Price { get; set; }
}
-
实现语法接收器
:语法接收器用于遍历语法树,查找带有
GenerateDto属性的类节点。
internal class SyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> DtoTypes { get; } = new List<ClassDeclarationSyntax>();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (!(syntaxNode is ClassDeclarationSyntax classDeclaration) || !classDeclaration.AttributeLists.Any())
{
return;
}
bool requiresGeneration = classDeclaration.AttributeLists.Count > 0 &&
classDeclaration.AttributeLists.SelectMany(_ => _.Attributes.Where(a => (a.Name as IdentifierNameSyntax).Identifier.Text == "GenerateDto")).Any();
if (requiresGeneration)
{
DtoTypes.Add(classDeclaration);
}
}
}
-
初始化源生成器
:在
Initialize方法中注册语法接收器。
[Generator]
public class MySourceGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
}
-
执行源生成器
:在
Execute方法中,检查上下文是否包含正确的接收器,然后遍历捕获的类声明,生成代码。
public void Execute(GeneratorExecutionContext context)
{
if (!(context.SyntaxReceiver is SyntaxReceiver receiver))
{
return;
}
foreach (ClassDeclarationSyntax classDeclaration in receiver.DtoTypes)
{
var properties = classDeclaration.DescendantNodes().OfType<PropertyDeclarationSyntax>();
var usings = classDeclaration.DescendantNodes().OfType<UsingDirectiveSyntax>();
var sourceBuilder = new StringBuilder();
foreach (UsingDirectiveSyntax usingDirective in usings)
{
sourceBuilder.AppendLine(usingDirective.FullSpan.ToString());
}
var className = classDeclaration.Identifier.ValueText;
var namespaceName = (classDeclaration.Parent as NamespaceDeclarationSyntax).Name.ToString();
sourceBuilder.AppendLine($"namespace {namespaceName}.Dto");
sourceBuilder.AppendLine("{");
sourceBuilder.Append($"public record {className} (");
foreach (PropertyDeclarationSyntax property in properties)
{
string propertyType = property.Type.ToString();
string propertyName = property.Identifier.ValueText;
sourceBuilder.Append($"{propertyType} {propertyName}, ");
}
sourceBuilder.Remove(sourceBuilder.Length - 2, 2);
sourceBuilder.Append(");");
sourceBuilder.AppendLine("}");
context.AddSource(classDeclaration.Identifier.ValueText, SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
}
- 将源生成器添加到项目 :将源生成器作为分析器添加到 csproj 文件中。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SourceGeneratorLibrary\SourceGeneratorLibrary.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
以下是编写源生成器的流程图:
graph TD;
A[创建 .NET Standard 2.0 类库] --> B[实现 ISourceGenerator 接口];
B --> C[创建 .NET 6 测试项目和属性];
C --> D[实现语法接收器];
D --> E[初始化源生成器];
E --> F[执行源生成器];
F --> G[将源生成器添加到项目];
通过以上介绍,我们了解了 Dapr 在分布式应用开发中的优势,以及 Roslyn 和源生成器在 .NET 代码分析和生成方面的强大功能。这些工具和技术能帮助开发者更高效地进行 .NET 开发,提升代码质量和性能。
.NET 开发中的 Dapr、Roslyn 与源生成器(续)
5. 源生成器的使用示例总结
在前面的内容中,我们详细介绍了编写源生成器的步骤,下面通过一个表格来总结整个过程:
|步骤|操作内容|代码示例|
| ---- | ---- | ---- |
|创建项目|在 .NET Standard 2.0 类库中实现
ISourceGenerator
接口,安装
Microsoft.CodeAnalysis
NuGet 包|
csharp<br>public interface ISourceGenerator<br>{<br> void Initialize(GeneratorInitializationContext context);<br> void Execute(GeneratorExecutionContext context);<br>}<br>
|
|定义测试项目和属性|创建 .NET 6 控制台应用,定义
GenerateDtoAttribute
并应用到类上|
csharp<br>internal class GenerateDtoAttribute : Attribute<br>{<br>}<br><br>[GenerateDto]<br>public class Product<br>{<br> public string Name { get; set; }<br> public string Description { get; set; }<br> public double Price { get; set; }<br>}<br>
|
|实现语法接收器|创建
SyntaxReceiver
类,遍历语法树查找带有
GenerateDto
属性的类节点|
csharp<br>internal class SyntaxReceiver : ISyntaxReceiver<br>{<br> public List<ClassDeclarationSyntax> DtoTypes { get; } = new List<ClassDeclarationSyntax>();<br> public void OnVisitSyntaxNode(SyntaxNode syntaxNode)<br> {<br> if (!(syntaxNode is ClassDeclarationSyntax classDeclaration) || !classDeclaration.AttributeLists.Any())<br> {<br> return;<br> }<br> bool requiresGeneration = classDeclaration.AttributeLists.Count > 0 &&<br> classDeclaration.AttributeLists.SelectMany(_ => _.Attributes.Where(a => (a.Name as IdentifierNameSyntax).Identifier.Text == "GenerateDto")).Any();<br> if (requiresGeneration)<br> {<br> DtoTypes.Add(classDeclaration);<br> }<br> }<br>}<br>
|
|初始化源生成器|在
Initialize
方法中注册语法接收器|
csharp<br>[Generator]<br>public class MySourceGenerator : ISourceGenerator<br>{<br> public void Initialize(GeneratorInitializationContext context)<br> {<br> context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());<br> }<br>}<br>
|
|执行源生成器|在
Execute
方法中,检查接收器,遍历类声明,生成代码|
csharp<br>public void Execute(GeneratorExecutionContext context)<br>{<br> if (!(context.SyntaxReceiver is SyntaxReceiver receiver))<br> {<br> return;<br> }<br> foreach (ClassDeclarationSyntax classDeclaration in receiver.DtoTypes)<br> {<br> var properties = classDeclaration.DescendantNodes().OfType<PropertyDeclarationSyntax>();<br> var usings = classDeclaration.DescendantNodes().OfType<UsingDirectiveSyntax>();<br> var sourceBuilder = new StringBuilder();<br> foreach (UsingDirectiveSyntax usingDirective in usings)<br> {<br> sourceBuilder.AppendLine(usingDirective.FullSpan.ToString());<br> }<br> var className = classDeclaration.Identifier.ValueText;<br> var namespaceName = (classDeclaration.Parent as NamespaceDeclarationSyntax).Name.ToString();<br> sourceBuilder.AppendLine($"namespace {namespaceName}.Dto");<br> sourceBuilder.AppendLine("{");<br> sourceBuilder.Append($"public record {className} (");<br> foreach (PropertyDeclarationSyntax property in properties)<br> {<br> string propertyType = property.Type.ToString();<br> string propertyName = property.Identifier.ValueText;<br> sourceBuilder.Append($"{propertyType} {propertyName}, ");<br> }<br> sourceBuilder.Remove(sourceBuilder.Length - 2, 2);<br> sourceBuilder.Append(");");<br> sourceBuilder.AppendLine("}");<br> context.AddSource(classDeclaration.Identifier.ValueText, SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));<br> }<br>}<br>
|
|将源生成器添加到项目|在 csproj 文件中添加项目引用,将源生成器作为分析器|
xml<br><Project Sdk="Microsoft.NET.Sdk"><br> <PropertyGroup><br> <OutputType>Exe</OutputType><br> <TargetFramework>net6.0</TargetFramework><br> <ImplicitUsings>enable</ImplicitUsings><br> </PropertyGroup><br> <ItemGroup><br> <ProjectReference Include="..\SourceGeneratorLibrary\SourceGeneratorLibrary.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /><br> </ItemGroup><br></Project><br>
|
6. 源生成器的使用效果验证
当我们完成源生成器的编写并添加到项目后,可以通过以下方式验证其是否正常工作:
1.
编译项目
:每次项目构建时,源生成器都会运行。
2.
使用反编译器检查
:使用如 ILSpy 这样的反编译器加载生成的程序集。检查后会发现,出现了
Dto
命名空间,并且该命名空间下为每个带有
GenerateDto
属性的类生成了对应的记录类型 DTO。
3.
代码中使用生成的对象
:我们可以在代码中实例化这些生成的 DTO 对象,示例如下:
var product = new Product("Introducing .NET 6", "Book by Apress about .NET 6", 50.0);
7. 综合应用与注意事项
在实际的 .NET 开发中,我们可以综合运用 Dapr、Roslyn 和源生成器来提升开发效率和代码质量。
-
Dapr 的应用场景
:在分布式应用开发中,Dapr 可以帮助我们简化组件管理和状态保存等操作。例如在微服务架构中,使用 Dapr 可以方便地处理服务间的通信、状态管理等问题。
-
Roslyn 的应用场景
:Roslyn 可以用于代码分析和重构。通过编写自定义的分析器和代码修复,可以确保团队代码符合统一的编码规范,提高代码的可读性和可维护性。
-
源生成器的应用场景
:源生成器可以在编译时生成额外的代码,避免运行时的反射操作,提高性能。例如在生成数据传输对象(DTO)、代理类等场景中非常有用。
同时,在使用这些工具和技术时,也需要注意以下几点:
-
Dapr
:虽然 Dapr 可以简化开发,但并不是所有项目都适合复杂的 Dapr 配置。在选择架构时,要根据项目的实际需求,避免过度设计。
-
Roslyn
:编写自定义分析器和代码修复时,需要对编译器原理和 API 有一定的了解。同时,要注意代码的性能,避免在分析过程中引入过多的开销。
-
源生成器
:源生成器只能生成和注入额外代码,不能修改开发者编写的代码。在编写源生成器时,要确保生成的代码符合项目的规范和需求。
以下是一个综合应用的流程图,展示了在一个 .NET 项目中如何结合使用这些技术:
graph LR;
A[项目规划] --> B{Dapr 适用?};
B -- 是 --> C[使用 Dapr 构建分布式应用];
B -- 否 --> D[传统架构开发];
C --> E{是否需要代码分析};
D --> E;
E -- 是 --> F[使用 Roslyn 编写分析器和代码修复];
E -- 否 --> G[正常开发];
F --> H{是否需要编译时代码生成};
G --> H;
H -- 是 --> I[使用源生成器生成额外代码];
H -- 否 --> J[完成开发];
I --> J;
通过合理运用 Dapr、Roslyn 和源生成器,开发者可以在 .NET 开发中更加高效地构建出高质量、高性能的应用程序。这些工具和技术为 .NET 开发带来了更多的可能性和灵活性,值得开发者深入学习和应用。
超级会员免费看
64

被折叠的 条评论
为什么被折叠?



