经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » ASP.net » 查看文章
使用.NET源生成器(SG)实现一个自动注入的生成器
来源:cnblogs  作者:万雅虎  时间:2024/5/6 16:14:39  对本文有异议

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

  1. services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
  2. services.AddTransient<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
  3. services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.ITest2Service, Biwen.AutoClassGen.TestConsole.Services.TestService2>();
  4. services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService3>();
  5. services.AddScoped<Biwen.AutoClassGen.TestConsole.Services2.MyService>();
  6. services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService>();
  7. services.AddSingleton<Biwen.AutoClassGen.TestConsole.Services.ITestService, Biwen.AutoClassGen.TestConsole.Services.TestService>();
  8. services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.ITest2Service, Biwen.AutoClassGen.TestConsole.Services.TestService>();

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

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

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

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

  1. namespace Biwen.AutoClassGen.Attributes
  2. {
  3. using System;
  4. /// <summary>
  5. /// 服务生命周期
  6. /// </summary>
  7. public enum ServiceLifetime
  8. {
  9. Singleton = 1,
  10. Transient = 2,
  11. Scoped = 4,
  12. }
  13. [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
  14. public class AutoInjectAttribute : Attribute
  15. {
  16. public ServiceLifetime ServiceLifetime { get; set; }
  17. public Type BaseType { get; set; }
  18. /// <summary>
  19. ///
  20. /// </summary>
  21. /// <param name="baseType">NULL表示服务自身</param>
  22. /// <param name="serviceLifetime">服务生命周期</param>
  23. public AutoInjectAttribute(Type baseType = null, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)
  24. {
  25. ServiceLifetime = serviceLifetime;
  26. BaseType = baseType;
  27. }
  28. }
  29. //C#11及以上的版本支持泛型Attribute
  30. #if NET7_0_OR_GREATER
  31. [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
  32. public class AutoInjectAttribute<T> : AutoInjectAttribute
  33. {
  34. public AutoInjectAttribute(ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) : base(typeof(T), serviceLifetime)
  35. {
  36. }
  37. }
  38. #endif
  39. }

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

  1. [AutoInject<TestService>]
  2. [AutoInject<ITestService>(ServiceLifetime.Singleton)]
  3. [AutoInject<ITest2Service>(ServiceLifetime.Scoped)]
  4. public class TestService : ITestService, ITest2Service
  5. {
  6. public string Say(string message)
  7. {
  8. return $"hello {message}";
  9. }
  10. public string Say2(string message)
  11. {
  12. return message;
  13. }
  14. }
  15. [AutoInject]
  16. [AutoInject(serviceLifetime: ServiceLifetime.Transient)]
  17. [AutoInject(typeof(ITest2Service), ServiceLifetime.Scoped)]
  18. public class TestService2 : ITest2Service
  19. {
  20. public string Say2(string message)
  21. {
  22. return message;
  23. }
  24. }

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

  1. private const string AttributeValueMetadataNameInject = "AutoInject";
  2. /// <summary>
  3. /// 泛型AutoInjectAttribute
  4. /// </summary>
  5. private const string GenericAutoInjectAttributeName = "Biwen.AutoClassGen.Attributes.AutoInjectAttribute`1";
  6. /// <summary>
  7. /// 非泛型AutoInjectAttribute
  8. /// </summary>
  9. private const string AutoInjectAttributeName = "Biwen.AutoClassGen.Attributes.AutoInjectAttribute";
  10. #region 非泛型
  11. //使用SyntaxProvider的ForAttributeWithMetadataName得到所有标注的服务集合
  12. var nodesAutoInject = context.SyntaxProvider.ForAttributeWithMetadataName(
  13. AutoInjectAttributeName,
  14. (context, attributeSyntax) => true,
  15. (syntaxContext, _) => syntaxContext.TargetNode).Collect();
  16. IncrementalValueProvider<(Compilation, ImmutableArray<SyntaxNode>)> compilationAndTypesInject =
  17. context.CompilationProvider.Combine(nodesAutoInject);
  18. #endregion
  19. #region 泛型
  20. var nodesAutoInjectG = context.SyntaxProvider.ForAttributeWithMetadataName(
  21. GenericAutoInjectAttributeName,
  22. (context, attributeSyntax) => true,
  23. (syntaxContext, _) => syntaxContext.TargetNode).Collect();
  24. IncrementalValueProvider<(Compilation, ImmutableArray<SyntaxNode>)> compilationAndTypesInjectG =
  25. context.CompilationProvider.Combine(nodesAutoInjectG);
  26. #endregion
  27. //合并所有的服务的编译类型
  28. var join = compilationAndTypesInject.Combine(compilationAndTypesInjectG);

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

  1. private record AutoInjectDefine
  2. {
  3. public string ImplType { get; set; } = null!;
  4. public string BaseType { get; set; } = null!;
  5. public string LifeTime { get; set; } = null!;
  6. }

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

  1. private static List<AutoInjectDefine> GetGenericAnnotatedNodesInject(Compilation compilation, ImmutableArray<SyntaxNode> nodes)
  2. {
  3. if (nodes.Length == 0) return [];
  4. // 注册的服务
  5. List<AutoInjectDefine> autoInjects = [];
  6. List<string> namespaces = [];
  7. foreach (ClassDeclarationSyntax node in nodes.AsEnumerable().Cast<ClassDeclarationSyntax>())
  8. {
  9. AttributeSyntax? attributeSyntax = null;
  10. foreach (var attr in node.AttributeLists.AsEnumerable())
  11. {
  12. var attrName = attr.Attributes.FirstOrDefault()?.Name.ToString();
  13. attributeSyntax = attr.Attributes.First(x => x.Name.ToString().IndexOf(AttributeValueMetadataNameInject, System.StringComparison.Ordinal) == 0);
  14. if (attrName?.IndexOf(AttributeValueMetadataNameInject, System.StringComparison.Ordinal) == 0)
  15. {
  16. //转译的Entity类名
  17. var baseTypeName = string.Empty;
  18. string pattern = @"(?<=<)(?<type>\w+)(?=>)";
  19. var match = Regex.Match(attributeSyntax.ToString(), pattern);
  20. if (match.Success)
  21. {
  22. baseTypeName = match.Groups["type"].Value.Split(['.']).Last();
  23. }
  24. else
  25. {
  26. continue;
  27. }
  28. var implTypeName = node.Identifier.ValueText;
  29. //var rootNamespace = node.AncestorsAndSelf().OfType<NamespaceDeclarationSyntax>().Single().Name.ToString();
  30. var symbols = compilation.GetSymbolsWithName(implTypeName);
  31. foreach (ITypeSymbol symbol in symbols.Cast<ITypeSymbol>())
  32. {
  33. implTypeName = symbol.ToDisplayString();
  34. break;
  35. }
  36. var baseSymbols = compilation.GetSymbolsWithName(baseTypeName);
  37. foreach (ITypeSymbol baseSymbol in baseSymbols.Cast<ITypeSymbol>())
  38. {
  39. baseTypeName = baseSymbol.ToDisplayString();
  40. break;
  41. }
  42. string lifeTime = "AddScoped"; //default
  43. {
  44. if (attributeSyntax.ArgumentList != null)
  45. {
  46. for (var i = 0; i < attributeSyntax.ArgumentList!.Arguments.Count; i++)
  47. {
  48. var expressionSyntax = attributeSyntax.ArgumentList.Arguments[i].Expression;
  49. if (expressionSyntax.IsKind(SyntaxKind.SimpleMemberAccessExpression))
  50. {
  51. var name = (expressionSyntax as MemberAccessExpressionSyntax)!.Name.Identifier.ValueText;
  52. lifeTime = name switch
  53. {
  54. "Singleton" => "AddSingleton",
  55. "Transient" => "AddTransient",
  56. "Scoped" => "AddScoped",
  57. _ => "AddScoped",
  58. };
  59. break;
  60. }
  61. }
  62. }
  63. autoInjects.Add(new AutoInjectDefine
  64. {
  65. ImplType = implTypeName,
  66. BaseType = baseTypeName,
  67. LifeTime = lifeTime,
  68. });
  69. //命名空间
  70. symbols = compilation.GetSymbolsWithName(baseTypeName);
  71. foreach (ITypeSymbol symbol in symbols.Cast<ITypeSymbol>())
  72. {
  73. var fullNameSpace = symbol.ContainingNamespace.ToDisplayString();
  74. // 命名空间
  75. if (!namespaces.Contains(fullNameSpace))
  76. {
  77. namespaces.Add(fullNameSpace);
  78. }
  79. }
  80. }
  81. }
  82. }
  83. }
  84. return autoInjects;
  85. }

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

  1. private static List<AutoInjectDefine> GetAnnotatedNodesInject(Compilation compilation, ImmutableArray<SyntaxNode> nodes)
  2. {
  3. if (nodes.Length == 0) return [];
  4. // 注册的服务
  5. List<AutoInjectDefine> autoInjects = [];
  6. List<string> namespaces = [];
  7. foreach (ClassDeclarationSyntax node in nodes.AsEnumerable().Cast<ClassDeclarationSyntax>())
  8. {
  9. AttributeSyntax? attributeSyntax = null;
  10. foreach (var attr in node.AttributeLists.AsEnumerable())
  11. {
  12. var attrName = attr.Attributes.FirstOrDefault()?.Name.ToString();
  13. attributeSyntax = attr.Attributes.FirstOrDefault(x => x.Name.ToString().IndexOf(AttributeValueMetadataNameInject, System.StringComparison.Ordinal) == 0);
  14. //其他的特性直接跳过
  15. if (attributeSyntax is null) continue;
  16. if (attrName?.IndexOf(AttributeValueMetadataNameInject, System.StringComparison.Ordinal) == 0)
  17. {
  18. var implTypeName = node.Identifier.ValueText;
  19. //var rootNamespace = node.AncestorsAndSelf().OfType<NamespaceDeclarationSyntax>().Single().Name.ToString();
  20. var symbols = compilation.GetSymbolsWithName(implTypeName);
  21. foreach (ITypeSymbol symbol in symbols.Cast<ITypeSymbol>())
  22. {
  23. implTypeName = symbol.ToDisplayString();
  24. break;
  25. }
  26. //转译的Entity类名
  27. var baseTypeName = string.Empty;
  28. if (attributeSyntax.ArgumentList == null || attributeSyntax.ArgumentList!.Arguments.Count == 0)
  29. {
  30. baseTypeName = implTypeName;
  31. }
  32. else
  33. {
  34. if (attributeSyntax.ArgumentList!.Arguments[0].Expression is TypeOfExpressionSyntax)
  35. {
  36. var eType = (attributeSyntax.ArgumentList!.Arguments[0].Expression as TypeOfExpressionSyntax)!.Type;
  37. if (eType.IsKind(SyntaxKind.IdentifierName))
  38. {
  39. baseTypeName = (eType as IdentifierNameSyntax)!.Identifier.ValueText;
  40. }
  41. else if (eType.IsKind(SyntaxKind.QualifiedName))
  42. {
  43. baseTypeName = (eType as QualifiedNameSyntax)!.ToString().Split(['.']).Last();
  44. }
  45. else if (eType.IsKind(SyntaxKind.AliasQualifiedName))
  46. {
  47. baseTypeName = (eType as AliasQualifiedNameSyntax)!.ToString().Split(['.']).Last();
  48. }
  49. if (string.IsNullOrEmpty(baseTypeName))
  50. {
  51. baseTypeName = implTypeName;
  52. }
  53. }
  54. else
  55. {
  56. baseTypeName = implTypeName;
  57. }
  58. }
  59. var baseSymbols = compilation.GetSymbolsWithName(baseTypeName);
  60. foreach (ITypeSymbol baseSymbol in baseSymbols.Cast<ITypeSymbol>())
  61. {
  62. baseTypeName = baseSymbol.ToDisplayString();
  63. break;
  64. }
  65. string lifeTime = "AddScoped"; //default
  66. {
  67. if (attributeSyntax.ArgumentList != null)
  68. {
  69. for (var i = 0; i < attributeSyntax.ArgumentList!.Arguments.Count; i++)
  70. {
  71. var expressionSyntax = attributeSyntax.ArgumentList.Arguments[i].Expression;
  72. if (expressionSyntax.IsKind(SyntaxKind.SimpleMemberAccessExpression))
  73. {
  74. var name = (expressionSyntax as MemberAccessExpressionSyntax)!.Name.Identifier.ValueText;
  75. lifeTime = name switch
  76. {
  77. "Singleton" => "AddSingleton",
  78. "Transient" => "AddTransient",
  79. "Scoped" => "AddScoped",
  80. _ => "AddScoped",
  81. };
  82. break;
  83. }
  84. }
  85. }
  86. autoInjects.Add(new AutoInjectDefine
  87. {
  88. ImplType = implTypeName,
  89. BaseType = baseTypeName,
  90. LifeTime = lifeTime,
  91. });
  92. //命名空间
  93. symbols = compilation.GetSymbolsWithName(baseTypeName);
  94. foreach (ITypeSymbol symbol in symbols.Cast<ITypeSymbol>())
  95. {
  96. var fullNameSpace = symbol.ContainingNamespace.ToDisplayString();
  97. // 命名空间
  98. if (!namespaces.Contains(fullNameSpace))
  99. {
  100. namespaces.Add(fullNameSpace);
  101. }
  102. }
  103. }
  104. }
  105. }
  106. }
  107. return autoInjects;
  108. }

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

  1. #pragma warning disable
  2. public static partial class AutoInjectExtension
  3. {
  4. /// <summary>
  5. /// 自动注册标注的服务
  6. /// </summary>
  7. /// <param name = "services"></param>
  8. /// <returns></returns>
  9. public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAutoInject(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
  10. {
  11. services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
  12. services.AddTransient<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
  13. // ...
  14. return services;
  15. }
  16. }
  17. #pragma warning restore

大致的代码如下:

  1. context.RegisterSourceOutput(join, (ctx, nodes) =>
  2. {
  3. var nodes1 = GetAnnotatedNodesInject(nodes.Left.Item1, nodes.Left.Item2);
  4. var nodes2 = GetGenericAnnotatedNodesInject(nodes.Right.Item1, nodes.Right.Item2);
  5. GenSource(ctx, [.. nodes1, .. nodes2]);
  6. });
  7. private static void GenSource(SourceProductionContext context, IEnumerable<AutoInjectDefine> injectDefines)
  8. {
  9. // 生成代码
  10. StringBuilder classes = new();
  11. injectDefines.Distinct().ToList().ForEach(define =>
  12. {
  13. if (define.ImplType != define.BaseType)
  14. {
  15. classes.AppendLine($@"services.{define.LifeTime}<{define.BaseType}, {define.ImplType}>();");
  16. }
  17. else
  18. {
  19. classes.AppendLine($@"services.{define.LifeTime}<{define.ImplType}>();");
  20. }
  21. });
  22. string rawNamespace = string.Empty;
  23. //_namespaces.Distinct().ToList().ForEach(ns => rawNamespace += $"using {ns};\r\n");
  24. var envSource = Template.Replace("$services", classes.ToString());
  25. envSource = envSource.Replace("$namespaces", rawNamespace);
  26. // format:
  27. envSource = FormatContent(envSource);
  28. context.AddSource($"Biwen.AutoClassGenInject.g.cs", SourceText.From(envSource, Encoding.UTF8));
  29. }
  30. /// <summary>
  31. /// 格式化代码
  32. /// </summary>
  33. /// <param name="csCode"></param>
  34. /// <returns></returns>
  35. private static string FormatContent(string csCode)
  36. {
  37. var tree = CSharpSyntaxTree.ParseText(csCode);
  38. var root = tree.GetRoot().NormalizeWhitespace();
  39. var ret = root.ToFullString();
  40. return ret;
  41. }
  42. private const string Template = """
  43. // <auto-generated />
  44. // issue:https://github.com/vipwan/Biwen.AutoClassGen/issues
  45. // 如果你在使用中遇到问题,请第一时间issue,谢谢!
  46. // This file is generated by Biwen.AutoClassGen.AutoInjectSourceGenerator
  47. #pragma warning disable
  48. $namespaces
  49. public static partial class AutoInjectExtension
  50. {
  51. /// <summary>
  52. /// 自动注册标注的服务
  53. /// </summary>
  54. /// <param name="services"></param>
  55. /// <returns></returns>
  56. public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAutoInject(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
  57. {
  58. $services
  59. return services;
  60. }
  61. }
  62. #pragma warning restore
  63. """;

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

  1. // <auto-generated />
  2. // issue:https://github.com/vipwan/Biwen.AutoClassGen/issues
  3. // 如果你在使用中遇到问题,请第一时间issue,谢谢!
  4. // This file is generated by Biwen.AutoClassGen.AutoInjectSourceGenerator
  5. #pragma warning disable
  6. public static partial class AutoInjectExtension
  7. {
  8. /// <summary>
  9. /// 自动注册标注的服务
  10. /// </summary>
  11. /// <param name = "services"></param>
  12. /// <returns></returns>
  13. public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAutoInject(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
  14. {
  15. services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
  16. services.AddTransient<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
  17. //...
  18. return services;
  19. }
  20. }
  21. #pragma warning restore

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

  1. dotnet add package Biwen.AutoClassGen

源代码我发布到了GitHub,欢迎star! https://github.com/vipwan/Biwen.AutoClassGen

原文链接:https://www.cnblogs.com/vipwan/p/18175230

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号