17、.NET 开发中的 Dapr、Roslyn 与源生成器

.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();
}
  1. 创建继承自 CSharpSyntaxWalker 的自定义类,例如 UsingDirectivesWalker ,用于遍历语法树并提取特定信息。
class UsingDirectivesWalker : CSharpSyntaxWalker
{
    public override void VisitUsingDirective(UsingDirectiveSyntax node)
    {
        Console.WriteLine($"Found using {node.Name}.");
    }
}
  1. 使用自定义语法 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);
}
  1. 定义测试项目和属性 :创建一个 .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; }
}
  1. 实现语法接收器 :语法接收器用于遍历语法树,查找带有 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);
        }
    }
}
  1. 初始化源生成器 :在 Initialize 方法中注册语法接收器。
[Generator]
public class MySourceGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }
}
  1. 执行源生成器 :在 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));
    }
}
  1. 将源生成器添加到项目 :将源生成器作为分析器添加到 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 开发带来了更多的可能性和灵活性,值得开发者深入学习和应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值