在 Fireasy3 揭秘 -- 依賴註入與服務發現 這篇中,我們通過遍列程式集中的所有類,來查找三個類型的服務介面,這樣應用在啟動時會消耗一定的時間來處理這些事情。今天,我們將用 `ISourceGenerator` 來對它進行改進。 ...
目錄
- Fireasy3 揭秘 -- 依賴註入與服務發現
- Fireasy3 揭秘 -- 自動服務部署
- Fireasy3 揭秘 -- 使用 SourceGeneraor 改進服務發現
- Fireasy3 揭秘 -- 使用 SourceGeneraor 實現動態代理(AOP)
在 Fireasy3 揭秘 -- 依賴註入與服務發現 這篇中,我們通過遍列程式集中的所有類,來查找三個類型的服務介面,這樣應用在啟動時會消耗一定的時間來處理這些事情。今天,我們將用 ISourceGenerator
來對它進行改進。
ISourceGenerator
是 Microsoft.CodeAnalysis.Analyzers
中的一項技術,它是基於代碼分析的原理,在語法樹中查找所需要的內容,通過這些內容再構造一段源代碼,使得我們在編譯程式集的時候,把這些代碼一併編譯進去。使用它的好處在於,它是在編譯時生成的,而不像 Emit
或其他反射等方法來構建的動態代碼一樣,在運行時將耗費一定的性能。
需要新建一個 .net standard 2.0
的項目,並引入 Microsoft.CodeAnalysis.Analyzers
和 Microsoft.CodeAnalysis.CSharp
,見 Fireasy.Common.Analyzers。
在項目里添加一個類,實現 ISourceGenerator
介面,如下:
[Generator]
public class ServiceDiscoverGenerator : ISourceGenerator
{
void ISourceGenerator.Initialize(GeneratorInitializationContext context)
{
Debugger.Launch();
context.RegisterForSyntaxNotifications(() => new ServiceDiscoverSyntaxReceiver());
}
void ISourceGenerator.Execute(GeneratorExecutionContext context)
{
}
}
Initialize
方法用於初始化生成器,使用 RegisterForSyntaxNotifications
方法向上下文註入一個語法接收器,以便用來分析語法樹。這裡的語法接收器有兩種,分別是 ISyntaxReceiver
和 ISyntaxContextReceiver
,後者可以從上下文中獲取到 SemanticModel
對象,這樣的話能夠從語法節點中獲取到定義的符號模型。使用符號模型相對於語法節點來說要更方便一些。下麵是基於 ISyntaxContextReceiver
介面的語法接收器。
internal class ServiceDiscoverSyntaxReceiver : ISyntaxContextReceiver
{
private const string SingletonServiceName = "Fireasy.Common.DependencyInjection.ISingletonService";
private const string TransientServiceName = "Fireasy.Common.DependencyInjection.ITransientService";
private const string ScopedServiceName = "Fireasy.Common.DependencyInjection.IScopedService";
private const string RegisterAttributeName = "Fireasy.Common.DependencyInjection.ServiceRegisterAttribute";
private List<ClassMetadata> _metadatas = new();
void ISyntaxContextReceiver.OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (context.Node is ClassDeclarationSyntax classSyntax)
{
AnalyseClassSyntax(context.SemanticModel, classSyntax);
}
}
}
OnVisitSyntaxNode
方法正如 lambda
表達式樹的 ExpressionVisitor
一樣,語法樹中的每一個節點都會被它訪問到。我們需要分析的是類,因此只需要處理 ClassDeclarationSyntax
語法即可。AnalyseClassSyntax
方法如下:
/// <summary>
/// 分析類型語法。
/// </summary>
/// <param name="model"></param>
/// <param name="syntax"></param>
private void AnalyseClassSyntax(SemanticModel model, ClassDeclarationSyntax syntax)
{
var typeSymbol = (ITypeSymbol)model.GetDeclaredSymbol(syntax)!;
var interfaces = typeSymbol.Interfaces;
//判斷是否使用了 特殊
var regAttr = typeSymbol.GetAttributes().FirstOrDefault(s => s.AttributeClass!.ToDisplayString() == RegisterAttributeName);
var lifetime = string.Empty;
if (regAttr != null)
{
lifetime = GetLifetime((int)regAttr.ConstructorArguments[0].Value!);
}
else if (interfaces.Any(s => s.ToDisplayString() == SingletonServiceName))
{
lifetime = "Singleton";
}
else if (interfaces.Any(s => s.ToDisplayString() == TransientServiceName))
{
lifetime = "Transient";
}
else if (interfaces.Any(s => s.ToDisplayString() == ScopedServiceName))
{
lifetime = "Scoped";
}
if (!string.IsNullOrEmpty(lifetime))
{
var serviceTypes = GetServiceTypes(interfaces).ToList();
//如果沒有實現任何介面,則判斷基類是不是抽象類,如果不是,則註冊自己
if (serviceTypes.Count == 0 && (typeSymbol.BaseType?.Name == "Object" || typeSymbol.BaseType?.IsAbstract == false))
{
serviceTypes.Add(typeSymbol);
}
_metadatas.Add(new ClassMetadata(typeSymbol, lifetime).AddServiceTypes(serviceTypes));
}
}
/// <summary>
/// 獲取生命周期。
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private string GetLifetime(int value) => value switch
{
0 => "Singleton",
1 => "Scoped",
2 => "Transient",
_ => string.Empty
};
/// <summary>
/// 從介面中篩選出服務類。
/// </summary>
/// <param name="types"></param>
/// <returns></returns>
private IEnumerable<ITypeSymbol> GetServiceTypes(IEnumerable<INamedTypeSymbol> types)
{
foreach (var type in types)
{
if (type.ToDisplayString() == SingletonServiceName ||
type.ToDisplayString() == TransientServiceName ||
type.ToDisplayString() == ScopedServiceName)
{
continue;
}
yield return type;
}
}
至此,我們就得到了一份可註冊的元數據,它由一個實現類對應多個服務類。ClassMetadata
的定義如下:
/// <summary>
/// 類的元數據。
/// </summary>
public class ClassMetadata
{
/// <summary>
/// 初始化 <see cref="ClassMetadata"/> 類的新實例。
/// </summary>
/// <param name="implementationType">實現類的類型。</param>
/// <param name="lifetime">生命周期。</param>
public ClassMetadata(ITypeSymbol implementationType, string lifetime)
{
ImplementationType = implementationType;
Lifetime = lifetime;
}
/// <summary>
/// 獲取實現類的類型。
/// </summary>
public ITypeSymbol ImplementationType { get; }
/// <summary>
/// 獲取服務類的類型列表。
/// </summary>
public List<ITypeSymbol> ServiceTypes { get; } = new();
/// <summary>
/// 獲取生命周期。
/// </summary>
public string Lifetime { get; }
/// <summary>
/// 添加服務類型。
/// </summary>
/// <param name="serviceTypes">服務類型列表。</param>
/// <returns></returns>
public ClassMetadata AddServiceTypes(IEnumerable<ITypeSymbol> serviceTypes)
{
ServiceTypes.AddRange(serviceTypes);
return this;
}
}
好了,得到這一份元數據後,我們轉到 ServiceDiscoverGenerator
,看看下一步它要做什麼。
[Generator]
public class ServiceDiscoverGenerator : ISourceGenerator
{
void ISourceGenerator.Initialize(GeneratorInitializationContext context)
{
Debugger.Launch();
context.RegisterForSyntaxNotifications(() => new ServiceDiscoverSyntaxReceiver());
}
void ISourceGenerator.Execute(GeneratorExecutionContext context)
{
if (context.SyntaxContextReceiver is ServiceDiscoverSyntaxReceiver receiver)
{
var metadatas = receiver.GetMetadatas();
if (metadatas.Count > 0)
{
context.AddSource("ServicesDiscover.cs", BuildDiscoverSourceCode(metadatas));
}
}
}
}
在 Execute
方法中,拿到接收器分析出來的元數據,通過 BuildDiscoverSourceCode
方法去生成一段源代碼。它是一個服務部署類,在 Configure
方法中,會把所有的服務描述添加到 IServiceCollection
容器內,如下:
private SourceText BuildDiscoverSourceCode(List<ClassMetadata> metadatas)
{
var sb = new StringBuilder();
sb.AppendLine(@"
using Fireasy.Common.DependencyInjection;
using Fireasy.Common.DynamicProxy;
using Microsoft.Extensions.DependencyInjection;
[assembly: Fireasy.Common.DependencyInjection.ServicesDeployAttribute(typeof(__ServiceDiscoverNs.__ServiceDiscoverServicesDeployer), Priority = 1)]
namespace __ServiceDiscoverNs
{
internal class __ServiceDiscoverServicesDeployer: IServicesDeployer
{
void IServicesDeployer.Configure(IServiceCollection services)
{");
foreach (var metadata in metadatas)
{
foreach (var svrType in metadata.ServiceTypes)
{
sb.AppendLine($" services.Add{metadata.Lifetime}(typeof({GetTypeName(svrType)}), typeof({GetTypeName(metadata.ImplementationType)}));");
}
}
sb.AppendLine(@"
}
}
}");
return SourceText.From(sb.ToString(), Encoding.UTF8);
}
private string GetTypeName(ITypeSymbol symbol)
{
if (symbol is INamedTypeSymbol namedTypeSymbol)
{
//如果是泛型,要處理成 Any<> 或 Any<,> 這樣的描述
if (namedTypeSymbol.IsGenericType)
{
var t = namedTypeSymbol.ToDisplayString();
return t.Substring(0, t.IndexOf("<") + 1) + new string(',', namedTypeSymbol.TypeArguments.Length - 1) + ">";
}
}
return symbol.ToDisplayString();
}
到這裡,源代碼生成器就算是完成了,那接下來怎麼讓它工作呢?
首先,我們需要找到一個“宿主”,我之所以這麼稱呼,是因為 nuget 打包時,需要將分析器依附到一個包內,因此我選擇 Fireasy.Common
,在 Fireasy.Common 的項目文件中,加下以下一段代碼,它的目的是當 Fireasy.Common
打包時,Fireasy.Common.Analyzers.dll
會自動打包到 analyzers 目錄下,引用 Fireasy.Common
包時,會自動使用該分析器來生成代碼。如下:
<Project Sdk="Microsoft.NET.Sdk">
<Target Name="_IncludeAllDependencies" BeforeTargets="_GetPackageFiles">
<ItemGroup>
<None Include="..\Fireasy.Common.Analyzers\bin\$(Configuration)\**\*.dll" Pack="True" PackagePath="analyzers\dotnet\cs" />
</ItemGroup>
</Target>
</Project>
我們測試的時候,因為是直接引用的項目,因此需要引用包含分析器的項目,而且要加上 OutputItemType
和 ReferenceOutputAssembly
,如下:
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\..\libraries\Fireasy.Common.Analyzers\Fireasy.Common.Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
好了,編譯測試項目,使用 ILSpy 反編譯 dll 文件,你會發現,實現了 ISingletonService
、ITransientService
或 IScopedService
的類自動註冊進來了:
// __ServiceDiscoverNs.__ServiceDiscoverServicesDeployer
using Fireasy.Common.DependencyInjection;
using Fireasy.Common.Tests;
using Microsoft.Extensions.DependencyInjection;
void IServicesDeployer.Configure(IServiceCollection services)
{
services.AddSingleton(typeof(DependencyInjectionTests.ITestSingletonService), typeof(DependencyInjectionTests.TestSingletonServiceImpl));
services.AddTransient(typeof(DependencyInjectionTests.ITestTransientService), typeof(DependencyInjectionTests.TestTransientServiceImpl));
services.AddScoped(typeof(DependencyInjectionTests.ITestScopedService), typeof(DependencyInjectionTests.TestScopedServiceImpl));
services.AddTransient(typeof(DependencyInjectionTests.ITestWithRegisterAttr), typeof(DependencyInjectionTests.TestWithRegisterAttrImpl));
services.AddTransient(typeof(DependencyInjectionTests.TestWithRegisterAttrNonIntefaceImpl), typeof(DependencyInjectionTests.TestWithRegisterAttrNonIntefaceImpl));
services.AddTransient(typeof(DependencyInjectionTests.IGenericService<, >), typeof(DependencyInjectionTests.GenericService<, >));
services.AddTransient(typeof(DependencyInjectionTests.TestDynamicProxyClass), typeof(DependencyInjectionTests.TestDynamicProxyClass));
services.AddTransient(typeof(ObjectActivatorTests.ITestService), typeof(ObjectActivatorTests.TestService));
}
另外還有一個小竅門,在測試項目的“依賴項”--“分析器”下,你會看到一個屬於自己的分析器,依次展開,也會找到所生成的那個代碼文件。
最後,奉上 Fireasy 3
的開源地址:https://gitee.com/faib920/fireasy3 ,歡迎大家前來捧場。
本文相關代碼請參考:
https://gitee.com/faib920/fireasy3/src/libraries/Fireasy.Common.Analyzers/ServiceDiscover
https://gitee.com/faib920/fireasy3/tests/Fireasy.Common.Tests/DependencyInjectionTests.cs
更多內容請移步官網 http://www.fireasy.cn 。
作者:fireasy出處:http://fireasy.cnblogs.com
官網:http://www.fireasy.cn
版權聲明:本文的版權歸作者與博客園共有。轉載時須註明本文的詳細鏈接,否則作者將保留追究其法律責任。