用 SourceGenerator 生成 注册 IoC / 配置的代码

之前运行在服务器上的程序 , 注册 IoC, Optons 都是通过扫描目录下的DLL, 然后遍历包含特定 Attribute 的类, 在跟据 Attribute 里的参数去注册.

AOT不支持动态加载, 同时会打包成一个文件, 所以不可能在去扫描DLL, 所以之前的方法, 基本行不通.

这里使用了 IIncrementalGenerator, 而不是 ISourceGenerator , 是因为这两个东西的支持文档都不友好 ,研究一个都耗费了好长时间.

    [Generator(LanguageNames.CSharp)]
    public class RegistServiceGenerator : IIncrementalGenerator
    {

        public void Initialize(IncrementalGeneratorInitializationContext context)
        {

            //Debugger.Launch();

            var provider = context.CompilationProvider.Select((compilation, cts) =>
            {

                var ns1 = new List<INamespaceSymbol>() {
                    compilation.Assembly.GlobalNamespace
                };

                var ns2 = compilation.SourceModule
                            .ReferencedAssemblySymbols
                            .Where(s => s.Name.Contains("CNB.PubTools"))
                            .Select(s => s.GlobalNamespace);

                var ns = ns1.Concat(ns2);

                var symbols = ns.SelectMany(s => Helper.GetAllTypeSymbol(s));

                return Helper.Transform(symbols.ToArray());
            });

            context.RegisterSourceOutput(provider, (spc, datas) =>
            {
                if (datas.Any())
                {
                    var code = Helper.GetCode(datas);
                    spc.AddSource("Regist.g.cs", code);
                }
            });
        }
    }

上面这段代码, 主要是:
1, 从 当前 及 当前引用的所有 的 Assembly 里找到所有的 GlobalNamespace
2, 遍历这些命名空间下的 Symbol
3, 把这些 Symbol 转换为中间数据。
4, 把中间数据通过 SourceOutput 生成代码。

遍历所有命名空间, 找到所有的 Symbol:

public static IEnumerable<INamedTypeSymbol> GetAllTypeSymbol(INamespaceSymbol namespaceSymbol)
{
    var typeMemberList = namespaceSymbol.GetTypeMembers();

    foreach (var typeSymbol in typeMemberList)
    {
        yield return typeSymbol;
    }

    foreach (var namespaceMember in namespaceSymbol.GetNamespaceMembers())
    {
        foreach (var typeSymbol in GetAllTypeSymbol(namespaceMember))
        {
            yield return typeSymbol;
        }
    }
}

转换为中间数据:

private static readonly string REGISTATTRIBUTE_NAME = "CNB.PubTools.Common.Attributes.RegistAttribute";
...
public static IEnumerable<RegistDescriptor> Transform(params INamedTypeSymbol[] symbols)
{
    if (symbols?.Any() != true)
        return Enumerable.Empty<RegistDescriptor>();

    var typeSymbols = symbols
                .Select(s => new
                {
                    ClassName = s.ToDisplayString(),
                    Attributes = s.GetAttributes().Where(a => a.AttributeClass.ToDisplayString() == REGISTATTRIBUTE_NAME || a.AttributeClass.BaseType.ToDisplayString() == REGISTATTRIBUTE_NAME)
                })
                .Where(s => s.Attributes?.Any() == true);

    var descriptors = new List<RegistDescriptor>();
    foreach (var typeSymbol in typeSymbols)
    {
        var tmp = typeSymbol.Attributes.Select(a =>
        {
            var name = a.AttributeClass.Name;
            return name switch
            {
                "RegistAttribute" => a.AttributeClass.IsGenericType
                    ? new RegistDescriptor()
                    {
                        Lifetime = (int)a.ConstructorArguments[0].Value,
                        ForType = a.AttributeClass.TypeArguments.First().ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
                        TargetType = typeSymbol.ClassName,
                        Tag = a.AttributeClass.Name
                    }
                    : new RegistDescriptor()
                    {
                        Lifetime = (int)a.ConstructorArguments[0].Value,
                        ForType = ((ITypeSymbol)a.ConstructorArguments[1].Value)?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) ?? typeSymbol.ClassName,
                        TargetType = typeSymbol.ClassName,
                        Tag = a.AttributeClass.Name
                    },
                "RegistInitializerAttribute" => new RegistDescriptor()
                {
                    TargetType = typeSymbol.ClassName,
                    Tag = a.AttributeClass.Name
                },
                "RegistViewAttribute" => new RegistDescriptor()
                {
                    TargetType = typeSymbol.ClassName,
                    Tag = a.AttributeClass.Name,
                    Name = typeSymbol.ClassName
                },
                _ => new RegistDescriptor(),
            };
        });
        descriptors.AddRange(tmp);
    }

    return descriptors;
}

.Where(a => a.AttributeClass.ToDisplayString() == REGISTATTRIBUTE_NAME || a.AttributeClass.BaseType.ToDisplayString() == REGISTATTRIBUTE_NAME)

这一段是判断当前 Symbol 是否包含特定的 Attribute, 或者 或父类是否是特定的 Attribute.

因为 RegistAttribute 会有很多变形,如果完全解析的话, 难度和时间成本太大, 所以这里采取了一个折衷的办法, 即 switch 那一段, 跟据不同的名称,生成不同的数据。

除了 ConstructorArguments 之外, 还有 NamedArguments, 但是这个东西的变形太大, 不方便在这里使用, 所以要求 RegistAttribute 要足够简单。

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class RegistAttribute : Attribute/*, INamed*/
{

    /// <summary>
    /// 模式
    /// </summary>
    public RegistMode Mode { get; }

    /// <summary>
    /// 注册为哪个类型
    /// </summary>
    public Type? ForType { get; }

    public RegistAttribute(RegistMode mode, Type? forType = null)
    {
        this.Mode = mode;
        this.ForType = forType;
    }
}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class RegistAttribute<T> : RegistAttribute
{
    public RegistAttribute(RegistMode mode) : base(mode, typeof(T))
    {
    }
}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class RegistInitializerAttribute : RegistAttribute
{
    /// <summary>
    /// 
    /// </summary>
    public RegistInitializerAttribute() : base(RegistMode.Singleton, typeof(IInitializer))
    {
    }
}

生成代码:

public static string GetCode(RegistDescriptor descriptor)
{
    if (string.IsNullOrWhiteSpace(descriptor.ForType))
        return $"RegistService(sc, typeof({descriptor.TargetType}), \"{descriptor.Tag}\", \"{descriptor.Name}\");";
    else
        return $"RegistService(sc, typeof({descriptor.ForType}), typeof({descriptor.TargetType}), {descriptor.Lifetime});";
}

public static string GetCode(IEnumerable<RegistDescriptor> ds)
{
    if (ds?.Any() != true)
        ds = Enumerable.Empty<RegistDescriptor>();

    var arr = ds.Select(GetCode).Distinct().OrderBy(s => s);
    var str = string.Join("\r\n            ", arr);

    var code = $$"""
            using CNB.PubTools.Common;
            using Microsoft.Extensions.DependencyInjection;
            using System;
            using System.Diagnostics.CodeAnalysis;

            namespace CNB.PubTools
            {
                internal partial class Regist
                {

                    public static partial void RegistService(IServiceCollection sc)
                    {
                        {{str}}
                    }
                }
            }
            """;

    return code;
}

结合上面的 SourceGenerator , 需要另外定义一个 partial 的类:

    /// <summary>
    /// 
    /// </summary>
    internal partial class Regist
    {

        /// <summary>
        /// 
        /// </summary>
        /// <param name="sc"></param>
        /// <param name="serviceType"></param>
        /// <param name="targetType"></param>
        /// <param name="mode"></param>
        private static void RegistService(IServiceCollection sc, Type serviceType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType, int mode)
        {
            switch ((RegistMode)mode)
            {
                case RegistMode.Scoped:
                    sc.AddScoped(serviceType, targetType);
                    break;
                case RegistMode.Singleton:
                    sc.AddSingleton(serviceType, targetType);
                    break;
                case RegistMode.PreRequest:
                    sc.AddTransient(serviceType, targetType);
                    break;
            }
        }

        private static void RegistService(IServiceCollection sc, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType, string tag, string? name)
        {
            switch (tag)
            {
                case "RegistInitializerAttribute":
                    sc.AddSingleton(typeof(IInitializer), targetType);
                    break;
                case "RegistViewAttribute":
                    sc.TryAddKeyedTransient(typeof(IView), name, targetType);
                    break;
            }
        }

        public static partial void RegistService(IServiceCollection sc);
    }

然后,在主项目里引用这个 SourceGenerator

<ProjectReference Include="..\CNB.PubTools.SourceGenerator\CNB.PubTools.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />

分析器项目, 不引用输出的 dll, OutputItemType="Analyzer" ReferenceOutputAssembly="false"

最后, 你可以在项目文件中添加:

<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>

把生成的代码输出到 obj 目录下面。
或者在如下的地方找到 SourceGenerator 生成的文件:


image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容