DI依賴注入對我們後端程序員來說肯定是基礎中的基礎了,我們經常會使用下面的代碼注入相關的service
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用於標註需要注入的類
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我們就可以給我們的服務打上標記了
[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
方法:
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的字段
private record AutoInjectDefine
{
public string ImplType { get; set; } = null!;
public string BaseType { get; set; } = null!;
public string LifeTime { get; set; } = null!;
}
解析所有的標註泛型的Attribute metadata
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集合
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轉換爲形如以下的代碼:
#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
大致的代碼如下:
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
""";
最終工具會自動爲你生成以下代碼:
// <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包體驗:
dotnet add package Biwen.AutoClassGen
源代碼我發佈到了GitHub,歡迎star! https://github.com/vipwan/Biwen.AutoClassGen