前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >.NET 源生成器 (SG) 实现自动注入生成器

.NET 源生成器 (SG) 实现自动注入生成器

作者头像
郑子铭
发布2024-05-28 17:02:57
1000
发布2024-05-28 17:02:57
举报

前言

在.NET开发领域,随着技术的不断演进,开发者一直在寻求提高代码质量和开发效率的方法。

源生成器(Source Generators,简称SG)作为.NET编译器平台Roslyn中的一项强大功能,为我们提供了一个独特的机会,可以在编译时生成或修改C#源代码。通过源生成器,我们可以自动化执行一些重复性的或复杂的任务,从而减少手写代码的数量,降低错误率,并提高整体的开发体验。

本文将介绍如何使用.NET源生成器实现一个自动注入的生成器。这个生成器能够根据预设的规则或配置,在编译时自动将所需的依赖项、代码片段或特性注入到目标项目中。通过这种方式,我们可以更加灵活地控制代码的生成过程,满足各种复杂的业务需求。

在深入探讨实现细节之前,我们需要先理解源生成器的基本原理和工作流程。源生成器是在编译过程中运行的代码分析器,它们能够读取源代码、解析语法树(Syntax Trees),并基于这些信息生成新的源代码文件或修改现有的源代码。这些生成的代码将在后续的编译阶段被当作普通的C#代码来处理。

通过本文的学习,将能够掌握使用.NET源生成器实现自动注入的基本方法,为项目开发带来新的可能性和灵感。无论你是经验丰富的.NET开发者,还是对源生成器技术感兴趣的新手,跟随本文一起探索新的技术领域。

正文

DI依赖注入对我们后端程序员来说肯定是基础中的基础了,我们经常会使用下面的代码注入相关的service

代码语言:javascript
复制
services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
services.AddTransient<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.ITest2Service, Biwen.AutoClassGen.TestConsole.Services.TestService2>();
services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService3>();
services.AddScoped<Biwen.AutoClassGen.TestConsole.Services2.MyService>();
services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService>();
services.AddSingleton<Biwen.AutoClassGen.TestConsole.Services.ITestService, Biwen.AutoClassGen.TestConsole.Services.TestService>();
services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.ITest2Service, Biwen.AutoClassGen.TestConsole.Services.TestService>();

对于上面的代码如果代码量很大 而且随着项目的迭代可能会堆积更多的代码,对于很多程序员来说第一想到的可能是透过反射批量注入,当然这也是最简单最直接的方式,今天我们使用源生成器的方式实现这个功能, 使用源生成器的方式好处还是有的 比如AOT需求,极致性能要求

实现这个功能的具体步骤:

定义Attribute-标注Attribute-遍历代码中标注Attribute的metadata集合-生成源代码

首先我们定义一个Attribute用于标注需要注入的类

代码语言:javascript
复制
namespace Biwen.AutoClassGen.Attributes
{
    using System;
    /// <summary>
    /// 服务生命周期
    /// </summary>
    public enum ServiceLifetime
    {
        Singleton = 1,
        Transient = 2,
        Scoped = 4,
    }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
    public class AutoInjectAttribute : Attribute
    {
        public ServiceLifetime ServiceLifetime { get; set; }
        public Type BaseType { get; set; }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="baseType">NULL表示服务自身</param>
        /// <param name="serviceLifetime">服务生命周期</param>
        public AutoInjectAttribute(Type baseType = null, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)
        {
            ServiceLifetime = serviceLifetime;
            BaseType = baseType;
        }
    }

//C#11及以上的版本支持泛型Attribute
#if NET7_0_OR_GREATER
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
    public class AutoInjectAttribute<T> : AutoInjectAttribute
    {
        public AutoInjectAttribute(ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) : base(typeof(T), serviceLifetime)
        {
        }
    }
#endif
}

通过上面定义的Attribute我们就可以给我们的服务打上标记了

代码语言:javascript
复制
[AutoInject<TestService>]
[AutoInject<ITestService>(ServiceLifetime.Singleton)]
[AutoInject<ITest2Service>(ServiceLifetime.Scoped)]
public class TestService : ITestService, ITest2Service
{
 public string Say(string message)
 {
  return $"hello {message}";
 }
 public string Say2(string message)
 {
  return message;
 }
}

[AutoInject]
[AutoInject(serviceLifetime: ServiceLifetime.Transient)]
[AutoInject(typeof(ITest2Service), ServiceLifetime.Scoped)]
public class TestService2 : ITest2Service
{
 public string Say2(string message)
 {
  return message;
 }
}

接下来就是Roslyn分析C#语法解析代码片段: 实现源生成器的唯一接口IIncrementalGenerator 实现Initialize方法:

代码语言:javascript
复制
private const string AttributeValueMetadataNameInject = "AutoInject";

/// <summary>
/// 泛型AutoInjectAttribute
/// </summary>
private const string GenericAutoInjectAttributeName = "Biwen.AutoClassGen.Attributes.AutoInjectAttribute`1";

/// <summary>
/// 非泛型AutoInjectAttribute
/// </summary>
private const string AutoInjectAttributeName = "Biwen.AutoClassGen.Attributes.AutoInjectAttribute";


#region 非泛型

//使用SyntaxProvider的ForAttributeWithMetadataName得到所有标注的服务集合
var nodesAutoInject = context.SyntaxProvider.ForAttributeWithMetadataName(
 AutoInjectAttributeName,
 (context, attributeSyntax) => true,
 (syntaxContext, _) => syntaxContext.TargetNode).Collect();

IncrementalValueProvider<(Compilation, ImmutableArray<SyntaxNode>)> compilationAndTypesInject =
 context.CompilationProvider.Combine(nodesAutoInject);

#endregion

#region 泛型

var nodesAutoInjectG = context.SyntaxProvider.ForAttributeWithMetadataName(
GenericAutoInjectAttributeName,
(context, attributeSyntax) => true,
(syntaxContext, _) => syntaxContext.TargetNode).Collect();

IncrementalValueProvider<(Compilation, ImmutableArray<SyntaxNode>)> compilationAndTypesInjectG =
 context.CompilationProvider.Combine(nodesAutoInjectG);

#endregion

//合并所有的服务的编译类型
var join = compilationAndTypesInject.Combine(compilationAndTypesInjectG);

解下来我们定义一个Metadata类,该类主要定义Attribute的字段

代码语言:javascript
复制
private record AutoInjectDefine
{
 public string ImplType { get; set; } = null!;
 public string BaseType { get; set; } = null!;
 public string LifeTime { get; set; } = null!;
}

解析所有的标注泛型的Attribute metadata

代码语言:javascript
复制
private static List<AutoInjectDefine> GetGenericAnnotatedNodesInject(Compilation compilation, ImmutableArray<SyntaxNode> nodes)
{
 if (nodes.Length == 0) return [];
 // 注册的服务
 List<AutoInjectDefine> autoInjects = [];
 List<string> namespaces = [];

 foreach (ClassDeclarationSyntax node in nodes.AsEnumerable().Cast<ClassDeclarationSyntax>())
 {
  AttributeSyntax? attributeSyntax = null;
  foreach (var attr in node.AttributeLists.AsEnumerable())
  {
   var attrName = attr.Attributes.FirstOrDefault()?.Name.ToString();
   attributeSyntax = attr.Attributes.First(x => x.Name.ToString().IndexOf(AttributeValueMetadataNameInject, System.StringComparison.Ordinal) == 0);

   if (attrName?.IndexOf(AttributeValueMetadataNameInject, System.StringComparison.Ordinal) == 0)
   {
    //转译的Entity类名
    var baseTypeName = string.Empty;

    string pattern = @"(?<=<)(?<type>\w+)(?=>)";
    var match = Regex.Match(attributeSyntax.ToString(), pattern);
    if (match.Success)
    {
     baseTypeName = match.Groups["type"].Value.Split(['.']).Last();
    }
    else
    {
     continue;
    }

    var implTypeName = node.Identifier.ValueText;
    //var rootNamespace = node.AncestorsAndSelf().OfType<NamespaceDeclarationSyntax>().Single().Name.ToString();
    var symbols = compilation.GetSymbolsWithName(implTypeName);
    foreach (ITypeSymbol symbol in symbols.Cast<ITypeSymbol>())
    {
     implTypeName = symbol.ToDisplayString();
     break;
    }

    var baseSymbols = compilation.GetSymbolsWithName(baseTypeName);
    foreach (ITypeSymbol baseSymbol in baseSymbols.Cast<ITypeSymbol>())
    {
     baseTypeName = baseSymbol.ToDisplayString();
     break;
    }

    string lifeTime = "AddScoped"; //default
    {
     if (attributeSyntax.ArgumentList != null)
     {
      for (var i = 0; i < attributeSyntax.ArgumentList!.Arguments.Count; i++)
      {
       var expressionSyntax = attributeSyntax.ArgumentList.Arguments[i].Expression;
       if (expressionSyntax.IsKind(SyntaxKind.SimpleMemberAccessExpression))
       {
        var name = (expressionSyntax as MemberAccessExpressionSyntax)!.Name.Identifier.ValueText;
        lifeTime = name switch
        {
         "Singleton" => "AddSingleton",
         "Transient" => "AddTransient",
         "Scoped" => "AddScoped",
         _ => "AddScoped",
        };
        break;
       }
      }
     }

     autoInjects.Add(new AutoInjectDefine
     {
      ImplType = implTypeName,
      BaseType = baseTypeName,
      LifeTime = lifeTime,
     });

     //命名空间
     symbols = compilation.GetSymbolsWithName(baseTypeName);
     foreach (ITypeSymbol symbol in symbols.Cast<ITypeSymbol>())
     {
      var fullNameSpace = symbol.ContainingNamespace.ToDisplayString();
      // 命名空间
      if (!namespaces.Contains(fullNameSpace))
      {
       namespaces.Add(fullNameSpace);
      }
     }
    }
   }
  }
 }

 return autoInjects;
}

解析所有标注非泛型Attribute的metadata集合

代码语言:javascript
复制
private static List<AutoInjectDefine> GetAnnotatedNodesInject(Compilation compilation, ImmutableArray<SyntaxNode> nodes)
{
 if (nodes.Length == 0) return [];
 // 注册的服务
 List<AutoInjectDefine> autoInjects = [];
 List<string> namespaces = [];

 foreach (ClassDeclarationSyntax node in nodes.AsEnumerable().Cast<ClassDeclarationSyntax>())
 {
  AttributeSyntax? attributeSyntax = null;
  foreach (var attr in node.AttributeLists.AsEnumerable())
  {
   var attrName = attr.Attributes.FirstOrDefault()?.Name.ToString();
   attributeSyntax = attr.Attributes.FirstOrDefault(x => x.Name.ToString().IndexOf(AttributeValueMetadataNameInject, System.StringComparison.Ordinal) == 0);

   //其他的特性直接跳过
   if (attributeSyntax is null) continue;

   if (attrName?.IndexOf(AttributeValueMetadataNameInject, System.StringComparison.Ordinal) == 0)
   {
    var implTypeName = node.Identifier.ValueText;
    //var rootNamespace = node.AncestorsAndSelf().OfType<NamespaceDeclarationSyntax>().Single().Name.ToString();
    var symbols = compilation.GetSymbolsWithName(implTypeName);
    foreach (ITypeSymbol symbol in symbols.Cast<ITypeSymbol>())
    {
     implTypeName = symbol.ToDisplayString();
     break;
    }

    //转译的Entity类名
    var baseTypeName = string.Empty;

    if (attributeSyntax.ArgumentList == null || attributeSyntax.ArgumentList!.Arguments.Count == 0)
    {
     baseTypeName = implTypeName;
    }
    else
    {
     if (attributeSyntax.ArgumentList!.Arguments[0].Expression is TypeOfExpressionSyntax)
     {
      var eType = (attributeSyntax.ArgumentList!.Arguments[0].Expression as TypeOfExpressionSyntax)!.Type;
      if (eType.IsKind(SyntaxKind.IdentifierName))
      {
       baseTypeName = (eType as IdentifierNameSyntax)!.Identifier.ValueText;
      }
      else if (eType.IsKind(SyntaxKind.QualifiedName))
      {
       baseTypeName = (eType as QualifiedNameSyntax)!.ToString().Split(['.']).Last();
      }
      else if (eType.IsKind(SyntaxKind.AliasQualifiedName))
      {
       baseTypeName = (eType as AliasQualifiedNameSyntax)!.ToString().Split(['.']).Last();
      }
      if (string.IsNullOrEmpty(baseTypeName))
      {
       baseTypeName = implTypeName;
      }
     }
     else
     {
      baseTypeName = implTypeName;
     }
    }


    var baseSymbols = compilation.GetSymbolsWithName(baseTypeName);
    foreach (ITypeSymbol baseSymbol in baseSymbols.Cast<ITypeSymbol>())
    {
     baseTypeName = baseSymbol.ToDisplayString();
     break;
    }

    string lifeTime = "AddScoped"; //default
    {
     if (attributeSyntax.ArgumentList != null)
     {
      for (var i = 0; i < attributeSyntax.ArgumentList!.Arguments.Count; i++)
      {
       var expressionSyntax = attributeSyntax.ArgumentList.Arguments[i].Expression;
       if (expressionSyntax.IsKind(SyntaxKind.SimpleMemberAccessExpression))
       {
        var name = (expressionSyntax as MemberAccessExpressionSyntax)!.Name.Identifier.ValueText;
        lifeTime = name switch
        {
         "Singleton" => "AddSingleton",
         "Transient" => "AddTransient",
         "Scoped" => "AddScoped",
         _ => "AddScoped",
        };
        break;
       }
      }
     }

     autoInjects.Add(new AutoInjectDefine
     {
      ImplType = implTypeName,
      BaseType = baseTypeName,
      LifeTime = lifeTime,
     });

     //命名空间
     symbols = compilation.GetSymbolsWithName(baseTypeName);
     foreach (ITypeSymbol symbol in symbols.Cast<ITypeSymbol>())
     {
      var fullNameSpace = symbol.ContainingNamespace.ToDisplayString();
      // 命名空间
      if (!namespaces.Contains(fullNameSpace))
      {
       namespaces.Add(fullNameSpace);
      }
     }
    }
   }
  }
 }
 return autoInjects;
}

通过上面的两个方法我们就取到了所有的Attribute的metadata,接下来的代码其实就比较简单了 原理就是将metadata转换为形如以下的代码:

代码语言:javascript
复制
#pragma warning disable
public static partial class AutoInjectExtension
{
    /// <summary>
    /// 自动注册标注的服务
    /// </summary>
    /// <param name = "services"></param>
    /// <returns></returns>
    public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAutoInject(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
    {
        services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
        services.AddTransient<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
       // ...
        return services;
    }
}
#pragma warning restore

大致的代码如下:

代码语言:javascript
复制
context.RegisterSourceOutput(join, (ctx, nodes) =>
{
 var nodes1 = GetAnnotatedNodesInject(nodes.Left.Item1, nodes.Left.Item2);
 var nodes2 = GetGenericAnnotatedNodesInject(nodes.Right.Item1, nodes.Right.Item2);
 GenSource(ctx, [.. nodes1, .. nodes2]);
});

private static void GenSource(SourceProductionContext context, IEnumerable<AutoInjectDefine> injectDefines)
{
 // 生成代码
 StringBuilder classes = new();
 injectDefines.Distinct().ToList().ForEach(define =>
 {
  if (define.ImplType != define.BaseType)
  {
   classes.AppendLine($@"services.{define.LifeTime}<{define.BaseType}, {define.ImplType}>();");
  }
  else
  {
   classes.AppendLine($@"services.{define.LifeTime}<{define.ImplType}>();");
  }
 });

 string rawNamespace = string.Empty;
 //_namespaces.Distinct().ToList().ForEach(ns => rawNamespace += $"using {ns};\r\n");
 var envSource = Template.Replace("$services", classes.ToString());
 envSource = envSource.Replace("$namespaces", rawNamespace);
 // format:
 envSource = FormatContent(envSource);
 context.AddSource($"Biwen.AutoClassGenInject.g.cs", SourceText.From(envSource, Encoding.UTF8));
}
/// <summary>
/// 格式化代码
/// </summary>
/// <param name="csCode"></param>
/// <returns></returns>
private static string FormatContent(string csCode)
{
 var tree = CSharpSyntaxTree.ParseText(csCode);
 var root = tree.GetRoot().NormalizeWhitespace();
 var ret = root.ToFullString();
 return ret;
}

private const string Template = """
 // <auto-generated />
 // issue:https://github.com/vipwan/Biwen.AutoClassGen/issues
 // 如果你在使用中遇到问题,请第一时间issue,谢谢!
 // This file is generated by Biwen.AutoClassGen.AutoInjectSourceGenerator
 #pragma warning disable
 $namespaces
 public static partial class AutoInjectExtension
 {
  /// <summary>
  /// 自动注册标注的服务
  /// </summary>
  /// <param name="services"></param>
  /// <returns></returns>
  public static  Microsoft.Extensions.DependencyInjection.IServiceCollection AddAutoInject(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
  {
   $services
   return services;
  }
 }
 
 #pragma warning restore
 """;

最终工具会自动为你生成以下代码:

代码语言:javascript
复制
// <auto-generated />
// issue:https://github.com/vipwan/Biwen.AutoClassGen/issues
// 如果你在使用中遇到问题,请第一时间issue,谢谢!
// This file is generated by Biwen.AutoClassGen.AutoInjectSourceGenerator
#pragma warning disable
public static partial class AutoInjectExtension
{
    /// <summary>
    /// 自动注册标注的服务
    /// </summary>
    /// <param name = "services"></param>
    /// <returns></returns>
    public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAutoInject(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
    {
        services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
        services.AddTransient<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
  //...
        return services;
    }
}
#pragma warning restore

以上代码就完成了整个源生成步骤,最后你可以使用我发布的nuget包体验:

代码语言:javascript
复制
dotnet add package Biwen.AutoClassGen

总结

总的来说,使用.NET源生成器实现自动注入的生成器是一项强大而灵活的技术。通过它,我们可以自动化执行一些重复性的或复杂的任务,减少手写代码的数量,降低错误率,并提高整体的开发体验。

无论是对于个人项目还是大型团队项目,源生成器都能够为我们带来显著的效益和便利。随着技术的不断发展,我们期待在未来看到更多基于源生成器的创新应用和实践。

地址

https://github.com/vipwan/Biwen.AutoClassGen

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-05-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 总结
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档