前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >使用 roslyn 的 Source Generator 自动完成依赖收集和注册

使用 roslyn 的 Source Generator 自动完成依赖收集和注册

作者头像
jgrass
发布2024-12-25 18:32:21
发布2024-12-25 18:32:21
6300
代码可运行
举报
文章被收录于专栏:蔻丁杂记蔻丁杂记
运行总次数:0
代码可运行

使用 Hosting 构建 WPF 程序 提到,因为不使用 Stylet 默认的 IOC 容器,所以不能自动收集和注册 View/ViewModel,需要动手处理。

如果项目比较大,手动处理显然过于麻烦。这里使用 roslyn 的 Source Generator 自动完成依赖收集和注册。

源码 JasonGrass/WpfAppTemplate1: WPF + Stylet + Hosting

新建分析器项目

以类库的模板,新建 WpfAppTemplate1.Generators,或者直接使用 Rider 新建。

代码语言:javascript
代码运行次数:0
复制
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>        <TargetFramework>netstandard2.0</TargetFramework>        <IsPackable>false</IsPackable>        <Nullable>enable</Nullable>        <LangVersion>latest</LangVersion>
        <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>        <IsRoslynComponent>true</IsRoslynComponent>
        <RootNamespace>WpfAppTemplate1.Generators</RootNamespace>        <PackageId>WpfAppTemplate1.Generators</PackageId>    </PropertyGroup>
    <ItemGroup>        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">            <PrivateAssets>all</PrivateAssets>            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>        </PackageReference>        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0"/>        <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0"/>    </ItemGroup>
</Project>

编写 SourceGenerator 代码

新建一个类,继承自 ISourceGenerator,并添加 Generator Attribute。

代码语言:javascript
代码运行次数:0
复制
using System.Collections.Generic;using System.Linq;using System.Text;using Microsoft.CodeAnalysis;using Microsoft.CodeAnalysis.CSharp;using Microsoft.CodeAnalysis.CSharp.Syntax;using Microsoft.CodeAnalysis.Text;
namespace WpfAppTemplate1.Generators;
[Generator]public class ViewDependencyInjectionGenerator : ISourceGenerator{    public void Initialize(GeneratorInitializationContext context) { }
    public void Execute(GeneratorExecutionContext context)    {        // System.Diagnostics.Debugger.Launch();
        // 获取所有语法树        var compilation = context.Compilation;        var syntaxTrees = context.Compilation.SyntaxTrees;
        // 查找目标类型(ViewModel和View)        var clsNodeList = syntaxTrees            .SelectMany(tree => tree.GetRoot().DescendantNodes())            .OfType<ClassDeclarationSyntax>()            .Where(cls =>                cls.Identifier.Text.EndsWith("ViewModel") || cls.Identifier.Text.EndsWith("View")            )            .Select(cls => new            {                ClassDeclaration = cls,                ModelSymbol = compilation.GetSemanticModel(cls.SyntaxTree).GetDeclaredSymbol(cls),            })            .ToList();
        // 生成注册代码        var sourceBuilder = new StringBuilder(            @"using Microsoft.Extensions.DependencyInjection;
public static class ViewModelDependencyInjection{    public static void AddViewModelServices(this IServiceCollection services)    {"        );
        HashSet<string> added = new HashSet<string>();
        foreach (var clsNode in clsNodeList)        {            if (clsNode.ModelSymbol == null)            {                continue;            }
            // var namespaceName = type.ModelSymbol.ContainingNamespace.ToDisplayString();            var fullName = clsNode.ModelSymbol.ToDisplayString(); // 包含命名空间的全称
            if (!added.Add(fullName))            {                // 避免因为 partial class 造成的重复添加                continue;            }
            // ViewModel 必须继承 Stylet.Screen            if (                clsNode.ClassDeclaration.Identifier.Text.EndsWith("ViewModel")                && InheritsFrom(clsNode.ModelSymbol, "Stylet.Screen")            )            {                sourceBuilder.AppendLine($"        services.AddSingleton<{fullName}>();");            }            // View 必须继承 System.Windows.FrameworkElement            else if (                clsNode.ClassDeclaration.Identifier.Text.EndsWith("View")                && InheritsFrom(clsNode.ModelSymbol, "System.Windows.FrameworkElement")            )            {                sourceBuilder.AppendLine($"        services.AddSingleton<{fullName}>();");            }        }
        sourceBuilder.AppendLine("    }");        sourceBuilder.AppendLine("}");
        var code = sourceBuilder.ToString();
        // 添加生成的代码到编译过程        context.AddSource(            "ViewModelDependencyInjection.g.cs",            SourceText.From(code, Encoding.UTF8)        );    }
    private bool InheritsFrom(INamedTypeSymbol typeSymbol, string baseClassName)    {        while (typeSymbol.BaseType != null)        {            if (typeSymbol.BaseType.ToDisplayString() == baseClassName)            {                return true;            }            typeSymbol = typeSymbol.BaseType;        }        return false;    }}

最终生成的代码如下:

代码语言:javascript
代码运行次数:0
复制
using Microsoft.Extensions.DependencyInjection;
public static class ViewModelDependencyInjection{    public static void AddViewModelServices(this IServiceCollection services)    {        services.AddSingleton<WpfAppTemplate1.View.RootView>();        services.AddSingleton<WpfAppTemplate1.ViewModel.RootViewModel>();    }}

这里没有指定命名空间,直接使用默认的命名空间。

在 WpfAppTemplate1 项目中使用

这里没有生成 nuget 包,直接使用项目引用

代码语言:javascript
代码运行次数:0
复制
  <ItemGroup>    <ProjectReference Include="..\WpfAppTemplate1.Generators\WpfAppTemplate1.Generators.csproj"  OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>  </ItemGroup>

OutputItemType="Analyzer" 表示将项目添加为分析器

ReferenceOutputAssembly="false" 表示此项目无需引用分析器项目的程序集

然后,在 Bootstrapper 中调用

代码语言:javascript
代码运行次数:0
复制
protected override void ConfigureIoC(IServiceCollection services){    base.ConfigureIoC(services);    // services.AddSingleton<RootViewModel>();    // services.AddSingleton<RootView>();
    services.AddViewModelServices();}

至此,大功告成。

可以在这里找到自动生成的代码

几个问题

1 编写完成之后没有生效

VS 对代码生成器的支持看起来还不是很好,尝试重启 VS;或者直接使用 Rider。

2 调试 source generator

对于新建的 source generator 项目,rider 会自动生成 launchSettings.json,可以直接启动项目进行调试

代码语言:javascript
代码运行次数:0
复制
{  "$schema": "https://json.schemastore.org/launchsettings.json",  "profiles": {    "DebugRoslynSourceGenerator": {      "commandName": "DebugRoslynComponent",      "targetProject": "../WpfAppTemplate1/WpfAppTemplate1.csproj"    }  }}

番外 - 使用 IIncrementalGenerator 优化 SourceGenerator 的性能

来自徳熙大佬的提示: 现在 VisualStudio 团队推荐使用增量的源代码生成器,因为现在这篇博客使用的源代码生成器让原本就卡慢的 Visual Studio 更加卡慢了。 新的增量源代码生成器是很好饯行不可变和增量模式的写法,可以使用更少的资源

尝试 IIncrementalGenerator 进行增量 Source Generator 生成代码 | 林德熙

代码语言:javascript
代码运行次数:0
复制
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading;using Microsoft.CodeAnalysis;using Microsoft.CodeAnalysis.CSharp.Syntax;using Microsoft.CodeAnalysis.Text;
namespace WpfAppTemplate1.Generators;
/* * 使用 IIncrementalGenerator 实现,优化 VS 调用性能 */
[Generator]internal class ViewDependencyInjectionGenerator2 : IIncrementalGenerator{    public void Initialize(IncrementalGeneratorInitializationContext context)    {        // 注册一个语法接收器,筛选出所有以 View 或 ViewModel 结尾的类声明        var classDeclarations = context            .SyntaxProvider.CreateSyntaxProvider(                predicate: IsCandidateClass, // 先通过语法筛选                transform: GetSemanticTarget // 再通过语义筛选            )            .Where(symbolAndClass => symbolAndClass.Symbol != null); // 过滤掉不符合条件
        // 收集所有符合条件的类的全名        var classFullNames = classDeclarations            .Select((symbolAndClass, ct) => symbolAndClass.Symbol!.ToDisplayString())            .Collect();
        // 当收集完成后,进行代码的生成        context.RegisterSourceOutput(            classFullNames,            (spc, fullNames) =>            {                if (fullNames.IsDefault || !fullNames.Any())                {                    // 如果没有符合条件的类,则不生成任何代码                    return;                }
                var sourceBuilder = new StringBuilder(                    @"using Microsoft.Extensions.DependencyInjection;
public static class ViewModelDependencyInjection{    public static void AddViewModelServices(this IServiceCollection services)    {"                );
                // 使用 HashSet 来避免重复添加                HashSet<string> added = new HashSet<string>();
                foreach (var fullName in fullNames.Distinct())                {                    if (added.Add(fullName))                    {                        sourceBuilder.AppendLine($"        services.AddSingleton<{fullName}>();");                    }                }
                sourceBuilder.AppendLine("    }");                sourceBuilder.AppendLine("}");
                // 将生成的代码添加到编译过程中                spc.AddSource(                    "ViewModelDependencyInjection.g.cs",                    SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)                );            }        );    }
    /// <summary>    /// 判断一个类声明是否是潜在的候选者(名称以 View 或 ViewModel 结尾)    /// </summary>    private static bool IsCandidateClass(SyntaxNode node, CancellationToken _)    {        return node is ClassDeclarationSyntax classDecl            && (                classDecl.Identifier.Text.EndsWith("View")                || classDecl.Identifier.Text.EndsWith("ViewModel")            );    }
    /// <summary>    /// 获取符合条件的类的符号信息    /// </summary>    private static (INamedTypeSymbol? Symbol, ClassDeclarationSyntax? ClassDecl) GetSemanticTarget(        GeneratorSyntaxContext context,        CancellationToken ct    )    {        var classDecl = (ClassDeclarationSyntax)context.Node;
        var symbol = context.SemanticModel.GetDeclaredSymbol(classDecl) as INamedTypeSymbol;
        if (symbol == null)            return (null, null);
        // 检查继承关系        if (classDecl.Identifier.Text.EndsWith("ViewModel"))        {            // ViewModel 必须继承 Stylet.Screen            if (!InheritsFrom(symbol, "Stylet.Screen"))                return (null, null);        }        else if (classDecl.Identifier.Text.EndsWith("View"))        {            // View 必须继承 System.Windows.FrameworkElement            if (!InheritsFrom(symbol, "System.Windows.FrameworkElement"))                return (null, null);        }        else        {            return (null, null);        }
        return (symbol, classDecl);    }
    /// <summary>    /// 判断一个符号是否继承自指定的基类    /// </summary>    private static bool InheritsFrom(INamedTypeSymbol typeSymbol, string baseClassFullName)    {        var current = typeSymbol.BaseType;        while (current != null)        {            if (current.ToDisplayString() == baseClassFullName)            {                return true;            }            current = current.BaseType;        }        return false;    }}

参考

SamplesInPractice/SourceGeneratorSample at main · WeihanLi/SamplesInPractice

使用 Source Generator 在编译你的 .NET 项目时自动生成代码 - walterlv

.net - C# Source Generator - warning CS8032: An instance of analyzer cannot be created - Stack Overflow

C# 源代码生成器的痛点:2022 年 2 月更新 - Turnerj(又名 James Turner) --- The pain points of C# source generators: February 2022 Update - Turnerj (aka. James Turner)

原文链接: https://cloud.tencent.com/developer/article/2481578

本作品采用 「署名 4.0 国际」 许可协议进行许可,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024年11月11日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 新建分析器项目
  • 编写 SourceGenerator 代码
  • 在 WpfAppTemplate1 项目中使用
  • 几个问题
  • 番外 - 使用 IIncrementalGenerator 优化 SourceGenerator 的性能
  • 参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档