Skip to content

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

Published: at 19:00

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

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

源码 JasonGrass/WpfAppTemplate1: WPF + Stylet + Hosting

新建分析器项目

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

<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。

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;
}
}

最终生成的代码如下:

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 包,直接使用项目引用

<ItemGroup>
<ProjectReference Include="..\WpfAppTemplate1.Generators\WpfAppTemplate1.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>

OutputItemType="Analyzer" 表示将项目添加为分析器
ReferenceOutputAssembly="false" 表示此项目无需引用分析器项目的程序集

然后,在 Bootstrapper 中调用

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,可以直接启动项目进行调试

{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"DebugRoslynSourceGenerator": {
"commandName": "DebugRoslynComponent",
"targetProject": "../WpfAppTemplate1/WpfAppTemplate1.csproj"
}
}
}

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

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

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

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://blog.jgrass.cc/posts/roslyn-source-generator-ioc/

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