<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      零基礎寫框架(1):從零設計一個模塊化和自動服務注冊框架

      關于從零設計 .NET 開發框架
      作者:癡者工良
      教程說明:

      倉庫地址:https://github.com/whuanle/maomi

      文檔地址:https://maomi.whuanle.cn

      作者博客:

      https://www.whuanle.cn

      http://www.rzrgm.cn/whuanle

      模塊化和自動服務注冊

      基于 ASP.NET Core 開發的 Web 框架中,最著名的是 ABP,ABP 主要特點之一開發不同項目(程序集)時,在每個項目中創建一個模塊類,程序加載每個程序集中,掃描出所有的模塊類,然后通過模塊類作為入口,初始化程序集。

      使用模塊化開發程序,好處是不需要關注程序集如何加載配置。開發人員開發程序集時,在模塊類中配置如何初始化、如何讀取配置,使用者只需要將模塊類引入進來即可,由框架自動啟動模塊類。

      Maomi.Core 也提供了模塊化開發的能力,同時還包括簡單易用的自動服務注冊。Maomi.Core 是一個很簡潔的包,可以在控制臺、Web 項目、WPF 項目中使用,在 WPF 項目中結合 MVVM 可以大量減少代碼復雜度,讓代碼更加清晰明朗。

      快速入手

      有 Demo1.Api、Demo1.Application 兩個項目,每個項目都有一個模塊類,模塊類需要實現 IModule 接口。

      image-20240218083153329

      Demo1.Application 項目的 ApplicationModule.cs 文件內容如下:

          public class ApplicationModule : IModule
          {
              // 模塊類中可以使用依賴注入
              private readonly IConfiguration _configuration;
              public ApplicationModule(IConfiguration configuration)
              {
                  _configuration = configuration;
              }
      
              public void ConfigureServices(ServiceContext services)
              {
                  // 這里可以編寫模塊初始化代碼
              }
          }
      

      如果要將服務注冊到容器中,在 class 上加上 [InjectOn] 特性即可。

          public interface IMyService
          {
              int Sum(int a, int b);
          }
      
          [InjectOn] // 自動注冊的標記
          public class MyService : IMyService
          {
              public int Sum(int a, int b)
              {
                  return a + b;
              }
          }
      

      上層模塊 Demo1.Api 中的 ApiModule.cs 可以通過特性注解引用底層模塊。

          [InjectModule<ApplicationModule>]
          public class ApiModule : IModule
          {
              public void ConfigureServices(ServiceContext services)
              {
                  // 這里可以編寫模塊初始化代碼
              }
          }
      

      最后,在程序啟動時配置模塊入口,并進行初始化。

      var builder = WebApplication.CreateBuilder(args);
      builder.Services.AddControllers();
      builder.Services.AddEndpointsApiExplorer();
      builder.Services.AddSwaggerGen();
      
      // 注冊模塊化服務,并設置 ApiModule 為入口
      builder.Services.AddModule<ApiModule>();
      
      var app = builder.Build();
      

      模塊可以依賴注入

      在 ASP.NET Core 配置 Host 時,會自動注入一些框架依賴的服務,如 IConfiguration 等,因此在 .AddModule<ApiModule>() 開始初始化模塊服務時,模塊獲取已經注入的服務。

      image-20240218164324287

      每個模塊都需要實現 IModule 接口,其定義如下:

          /// <summary>
          /// 模塊接口
          /// </summary>
          public interface IModule
          {
              /// <summary>
              /// 模塊中的依賴注入
              /// </summary>
              /// <param name="context">模塊服務上下文</param>
              void ConfigureServices(ServiceContext context);
          }
      

      除了可以直接在模塊構造函數注入服務之外,還可以通過 ServiceContext context 獲取服務和配置。

          /// <summary>
          /// 模塊上下文
          /// </summary>
          public class ServiceContext
          {
              private readonly IServiceCollection _serviceCollection;
              private readonly IConfiguration _configuration;
      
      
              internal ServiceContext(IServiceCollection serviceCollection, IConfiguration configuration)
              {
                  _serviceCollection = serviceCollection;
                  _configuration = configuration;
              }
      
              /// <summary>
              /// 依賴注入服務
              /// </summary>
              public IServiceCollection Services => _serviceCollection;
      
              /// <summary>
              /// 配置
              /// </summary>
              public IConfiguration Configuration => _configuration;
          }
      

      模塊化

      因為模塊之間會有依賴關系,為了識別這些依賴關系,Maomi.Core 使用樹來表達依賴關系。Maomi.Core 在啟動模塊服務時,掃描所有模塊類,然后將模塊依賴關系存放到模塊樹中,然后按照左序遍歷的算法對模塊逐個初始化,也就是先從底層模塊開始進行初始化。

      循環依賴檢測

      Maomi.Core 可以識別模塊循環依賴

      比如,有以下模塊和依賴:

      [InjectModule<A>()]
      [InjectModule<B>()]
      class C:IModule
      
      [InjectModule<A>()]
      class B:IModule
      
      // 這里出現了循環依賴
      [InjectModule<C>()]
      class A:IModule
      
      // C 是入口模塊
      services.AddModule<C>();
      

      因為 C 模塊依賴 A、B 模塊,所以 A、B 是節點 C 的子節點,而 A、B 的父節點則是 C。當把 A、B、C 三個模塊以及依賴關系掃描完畢之后,會得到以下的模塊依賴樹。

      如下圖所示,每個模塊都做了下標,表示不同的依賴關系,一個模塊可以出現多次,C1 -> A0 表示 C 依賴 A。

      image-20240218165015839

      C0 開始,沒有父節點,則不存在循環依賴。

      從 A0 開始,A0 -> C0 ,該鏈路中也沒有出現重復的 A 模塊。

      從 C1 開始,C1 -> A0 -> C0 ,該鏈路中 C 模塊重復出現,則說明出現了循環依賴。

      從 C2 開始,C2 -> A1 -> B0 -> C0 ,該鏈路中 C 模塊重復出現,則說明出現了循環依賴。

      模塊初始化順序

      在生成模塊樹之后,通過對模塊樹進行后序遍歷即可。

      比如,有以下模塊以及依賴。

      [InjectModule<C>()]
      [InjectModule<D>()]
      class E:IModule
      
      [InjectModule<A>()]
      [InjectModule<B>()]
      class C:IModule
      
      [InjectModule<B>()]
      class D:IModule
          
      [InjectModule<A>()]
      class B:IModule
          
      class A:IModule
      
      // E 是入口模塊
      services.AddModule<E>();
      

      生成模塊依賴樹如圖所示:

      111

      首先從 E0 開始掃描,因為 E0 下存在子節點 C0、 D0,那么就會先順著 C0 再次掃描,掃描到 A0 時,因為 A0 下已經沒有子節點了,所以會對 A0 對應的模塊 A 進行初始化。根據上圖模塊依賴樹進行后序遍歷,初始化模塊的順序是(已經被初始化的模塊會跳過):

      111

      服務自動注冊

      Maomi.Core 是通過 [InjectOn] 識別要注冊該服務到容器中,其定義如下:

          /// <summary>
          /// 依賴注入標記
          /// </summary>
          [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
          public class InjectOnAttribute : Attribute
          {
              /// <summary>
              /// 要注入的服務
              /// </summary>
              public Type[]? ServicesType { get; set; }
      
              /// <summary>
              /// 生命周期
              /// </summary>
              public ServiceLifetime Lifetime { get; set; }
      
              /// <summary>
              /// 注入模式
              /// </summary>
              public InjectScheme Scheme { get; set; }
      
              /// <summary>
              /// 是否注入自己
              /// </summary>
              public bool Own { get; set; } = false;
      
              /// <summary>
              /// 
              /// </summary>
              /// <param name="lifetime"></param>
              /// <param name="scheme"></param>
              public InjectOnAttribute(ServiceLifetime lifetime = ServiceLifetime.Transient, InjectScheme scheme = InjectScheme.OnlyInterfaces)
              {
                  Lifetime = lifetime;
                  Scheme = scheme;
              }
          }
      

      使用 [InjectOn] 時,默認是注冊服務為 Transient 生命周期,且注冊所有接口。

          [InjectOn]
          public class MyService : IAService, IBService
      

      等同于:

      services.AddTransient<IAService, MyService>();
      services.AddTransient<IBService, MyService>();
      

      如果只想注冊 IAService,可以將注冊模式設置為InjectScheme.Some ,然后自定義注冊的類型:

          [InjectOn(
              lifetime: ServiceLifetime.Transient,
              Scheme = InjectScheme.Some,
              ServicesType = new Type[] { typeof(IAService) }
              )]
          public class MyService : IAService, IBService
      

      也可以把自身注冊到容器中:

      	[InjectOn(Own = true)]
      	public class MyService : IMyService
      

      等同于:

      services.AddTransient<IAService, MyService>();
      services.AddTransient<MyService>();
      

      如果服務繼承了類、接口,只想注冊父類,那么可以這樣寫:

          public class ParentService { }
      
          [InjectOn(
              Scheme = InjectScheme.OnlyBaseClass
              )]
          public class MyService : ParentService, IDisposable 
      

      等同于:

      services.AddTransient<ParentService, MyService>();
      services.AddTransient<MyService>();
      

      如果只注冊自身,忽略接口等,可以使用:

      [InjectOn(ServiceLifetime.Scoped, Scheme = InjectScheme.None, Own = true)]
      

      模塊化和自動服務注冊的設計和實現

      在本小節中,我們將會開始設計一個支持模塊化和自動服務注冊的小框架,從設計和實現 Maomi.Core 開始,我們在后面的章節中會掌握更多框架技術的設計思路和實現方法,從而掌握從零開始編寫一個框架的能力。

      項目說明

      創建一個名為 Maomi.Core 的類庫項目,這個類庫中將會包含框架核心抽象和實現代碼。

      為了減少命名空間長度,便于開發的時候引入需要的命名空間,打開 Maomi.Core.csproj 文件,在 PropertyGroup 屬性中,添加一行配置:

      <RootNamespace>Maomi</RootNamespace>
      

      配置 <RootNamespace> 屬性之后,我們在 Maomi.Core 項目中創建的類型,其命名空間都會以 Maomi. 開頭,而不是 Maomi.Core。

      接著為項目添加兩個依賴包,以便實現自動依賴注入和初始化模塊時提供配置。

      Microsoft.Extensions.DependencyInjection
      Microsoft.Extensions.Configuration.Abstractions
      

      模塊化設計

      當本章的代碼編寫完畢之后,我們可以這樣實現一個模塊、初始化模塊、引入依賴模塊。代碼示例如下:

          [InjectModule<ApplicationModule>]
          public class ApiModule : IModule
          {
              private readonly IConfiguration _configuration;
              public ApiModule(IConfiguration configuration)
              {
                  _configuration = configuration;
              }
      
              public void ConfigureServices(ServiceContext context)
              {
                  var configuration = context.Configuration;
                  context.Services.AddCors();
              }
          }
      

      從這段代碼,筆者以從上到下的順序來解讀我們需要實現哪些技術點。

      1,模塊依賴。

      [InjectModule<ApplicationModule>] 表示當前模塊需要依賴哪些模塊。如果需要依賴多個模塊,可以使用多個特性,示例如下:

      [InjectModule<DomainModule>]
      [InjectModule<ApplicationModule>]
      

      2,模塊接口和初始化。

      每一個模塊都需要實現 IModule 接口,框架識別到類型繼承了這個接口后才會把類型當作一個模塊類進行處理。IModule 接口很簡單,只有 ConfigureServices(ServiceContext context) 一個方法,可以在這個方法中編寫初始化模塊的代碼。ConfigureServices 方法中有一個 ServiceContext 類型的參數, ServiceContext 中包含了 IServiceCollection、IConfiguration ,模塊可以從 ServiceContext 中獲得當前容器的服務、啟動時的配置等。

      3,依賴注入

      每個模塊的構造函數都可以使用依賴注入,可以在模塊類中注入需要的服務,開發者可以在模塊初始化時,通過這些服務初始化模塊。

      基于以上三點,我們可以先抽象出特性類、接口等,由于這些類型不包含具體的邏輯,因此從這一部分先下手,實現起來會更簡單,可以避免大腦混亂,編寫框架時不知道要從哪里先下手。

      創建一個 ServiceContext 類,用于在模塊間傳遞服務上下文信息,其代碼如下:

          public class ServiceContext
          {
              private readonly IServiceCollection _serviceCollection;
              private readonly IConfiguration _configuration;
      
              internal ServiceContext(IServiceCollection serviceCollection, IConfiguration configuration)
              {
                  _serviceCollection = serviceCollection;
                  _configuration = configuration;
              }
      
              public IServiceCollection Services => _serviceCollection;
              public IConfiguration Configuration => _configuration;
          }
      

      根據實際需求,還可以在 ServiceContext 中添加日志等屬性字段。

      創建 IModule 接口。

          public interface IModule
          {
              void ConfigureServices(ServiceContext services);
          }
      

      創建 InjectModuleAttribute 特性,用于引入依賴模塊。

          [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
          public class InjectModuleAttribute : Attribute
          {
              // 依賴的模塊
              public Type ModuleType { get; private init; }
              public InjectModuleAttribute(Type type)
              {
                  ModuleType = type;
              }
          }
      
          [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
          public sealed class InjectModuleAttribute<TModule> : InjectModuleAttribute
              where TModule : IModule
          {
              public InjectModuleAttribute() : base(typeof(TModule)){}
          }
      

      泛型特性屬于 C# 11 的新語法。

      定義兩個特性類后,我們可以使用 [InjectModule(typeof(AppModule))]InjectModule<AppModule> 的方式定義依賴模塊。

      自動服務注冊的設計

      當完成本章的代碼編寫后,如果需要注入服務,只需要標記 [InjectOn] 特性即可。

      // 簡單注冊
      [InjectOn]
      public class MyService : IMyService
      // 注注冊并設置生命周期為 scope
      [InjectOn(ServiceLifetime.Scoped)]
      public class MyService : IMyService
      
      // 只注冊接口,不注冊父類
      [InjectOn(InjectScheme.OnlyInterfaces)]
      public class MyService : ParentService, IMyService
      

      有時我們會有各種各樣的需求,例如 MyService 繼承了父類 ParentService 和接口 IMyService,但是只需要注冊 ParentService,而不需要注冊接口;又或者只需要注冊 MyService,而不需要注冊 ParentServiceIMyService。

      創建 InjectScheme 枚舉,定義注冊模式:

          public enum InjectScheme
          {
              // 注入父類、接口
              Any,
              
              // 手動選擇要注入的服務
              Some,
              
              // 只注入父類
              OnlyBaseClass,
              
              // 只注入實現的接口
              OnlyInterfaces,
              
              // 此服務不會被注入到容器中
              None
          }
      

      定義服務注冊特性:

          // 依賴注入標記
          [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
          public class InjectOnAttribute : Attribute
          {
              // 要注入的服務
              public Type[]? ServicesType { get; set; }
              
              // 生命周期
              public ServiceLifetime Lifetime { get; set; }
              
              // 注入模式
              public InjectScheme Scheme { get; set; }
      
              // 是否注入自己
              public bool Own { get; set; } = false;
              
              public InjectOnAttribute(ServiceLifetime lifetime = ServiceLifetime.Transient, 
                                       InjectScheme scheme = InjectScheme.OnlyInterfaces)
              {
                  Lifetime = lifetime;
                  Scheme = scheme;
              }
          }
      

      模塊依賴

      因為模塊之間會有依賴關系,因此為了生成模塊樹,需要定義一個 ModuleNode 類表示模塊節點,一個 ModuleNode 實例標識一個依賴關系。

          /// <summary>
          /// 模塊節點
          /// </summary>
          internal class ModuleNode
          {
              // 當前模塊類型
              public Type ModuleType { get; set; } = null!;
      
              // 鏈表,指向父模塊節點,用于循環引用檢測
              public ModuleNode? ParentModule { get; set; }
              
              // 依賴的其它模塊
              public HashSet<ModuleNode>? Childs { get; set; }
      
              // 通過鏈表檢測是否出現了循環依賴
              public bool ContainsTree(ModuleNode childModule)
              {
                  if (childModule.ModuleType == ModuleType) return true;
                  if (this.ParentModule == null) return false;
                  // 如果當前模塊找不到記錄,則向上查找
                  return this.ParentModule.ContainsTree(childModule);
              }
      
              public override int GetHashCode()
              {
                  return ModuleType.GetHashCode();
              }
      
              public override bool Equals(object? obj)
              {
                  if (obj == null) return false;
                  if(obj is ModuleNode module)
                  {
                      return GetHashCode() == module.GetHashCode();
                  }
                  return false;
              }
          }
      

      框架在掃描所有程序集之后,通過 ModuleNode 實例將所有模塊以及模塊依賴組成一顆模塊樹,通過模塊樹來判斷是否出現了循環依賴。

      比如,有以下模塊和依賴:

      [InjectModule<A>()]
      [InjectModule<B>()]
      class C:IModule
      
      [InjectModule<A>()]
      class B:IModule
      
      // 這里出現了循環依賴
      [InjectModule<C>()]
      class A:IModule
      
      // C 是入口模塊
      services.AddModule<C>();
      

      因為 C 模塊依賴 A、B 模塊,所以 A、B 是節點 C 的子節點,而 A、B 的父節點則是 C。

      C.Childs = new (){ A , B}
      
      A.ParentModule => C
      B.ParentModule => C
      

      當把 A、B、C 三個模塊以及依賴關系掃描完畢之后,會得到以下的模塊依賴樹。一個節點即是一個 ModuleNode 實例,一個模塊被多次引入,就會出現多次。

      依賴1

      那么,如果識別到循環依賴呢?只需要調用 ModuleNode.ContainsTree()從一個 ModuleNode 實例中,不斷往上查找 ModuleNode.ParentModule 即可,如果該鏈表中包含相同類型的模塊,即為循環依賴,需要拋出異常。

      比如從 C0 開始,沒有父節點,則不存在循環依賴。

      從 A0 開始,A0 -> C0 ,該鏈路中也沒有出現重復的 A 模塊。

      從 C1 開始,C1 -> A0 -> C0 ,該鏈路中 C 模塊重復出現,則說明出現了循環依賴。

      所以,是否出現了循環依賴判斷起來是很簡單的,我們只需要從 ModuleNode.ContainsTree() 往上查找即可。

      在生成模塊樹之后,通過對模塊樹進行后序遍歷即可。

      比如,有以下模塊以及依賴。

      [InjectModule<C>()]
      [InjectModule<D>()]
      class E:IModule
      
      [InjectModule<A>()]
      [InjectModule<B>()]
      class C:IModule
      
      [InjectModule<B>()]
      class D:IModule
          
      [InjectModule<A>()]
      class B:IModule
          
      class A:IModule
      
      // E 是入口模塊
      services.AddModule<E>();
      

      生成模塊依賴樹如圖所示:

      依賴2

      首先從 E0 開始掃描,因為 E0 下存在子節點 C0、 D0,那么就會先順著 C0 再次掃描,掃描到 A0 時,因為 A0 下已經沒有子節點了,所以會對 A0 對應的模塊 A 進行初始化。根據上圖模塊依賴樹進行后序遍歷,初始化模塊的順序是(已經被初始化的模塊會跳過):

      初始化順序

      偽代碼示例如下:

      		private static void InitModuleTree(ModuleNode moduleNode)
      		{
      			if (moduleNode.Childs != null)
      			{
      				foreach (var item in moduleNode.Childs)
      				{
      					InitModuleTree(item);
      				}
      			}
                  
                  // 如果該節點已經沒有子節點
      			// 如果模塊沒有處理過
      			if (!moduleTypes.Contains(moduleNode.ModuleType))
      			{
      				InitInjectService(moduleNode.ModuleType);
      			}
      		}
      

      實現模塊化和自動服務注冊

      本小節的代碼都在 ModuleExtensions.cs 中。

      當我們把接口、枚舉、特性等類型定義之后,接下來我們便要思考如何實例化模塊、檢測模塊的依賴關系,實現自動服務注冊。為了簡化設計,我們可以將模塊化自動服務注冊寫在一起,當初始化一個模塊時,框架同時會掃描該程序集中的服務進行注冊。如果程序集中不包含模塊類,那么框架不會掃描該程序集,也就不會注冊服務。

      接下來,我們思考模塊化框架需要解決哪些問題或支持哪些功能:

      • 如何識別和注冊服務;

      • 框架能夠識別模塊的依賴,生成模塊依賴樹,能夠檢測到循環依賴等問題;

      • 多個模塊可能引用了同一個模塊 A,但是模塊 A 只能被實例化一次;

      • 初始化模塊的順序;

      • 模塊類本身要作為服務注冊到容器中,實例化模塊類時,需要支持依賴注入,也就是說模塊類的構造函數可以注入其它服務;

      我們先解決第一個問題,

      因為自動服務注冊是根據模塊所在的程序集掃描標記類,識別所有使用了 InjectOnAttribute 特性的類型,所以我們可以先編寫一個程序集掃描方法,該方法的功能是通過程序集掃描所有類型,然后根據特性配置注冊服務。

      /// <summary>
      /// 自動依賴注入
      /// </summary>
      /// <param name="services"></param>
      /// <param name="assembly"></param>
      /// <param name="injectTypes">已被注入的服務</param>
      private static void InitInjectService(IServiceCollection services, Assembly assembly, HashSet<Type> injectTypes)
      {
      	// 只掃描可實例化的類,不掃描靜態類、接口、抽象類、嵌套類、非公開類等
      	foreach (var item in assembly.GetTypes().Where(x => x.IsClass && !x.IsAbstract && !x.IsNestedPublic))
      	{
      		var inject = item.GetCustomAttributes().FirstOrDefault(x => x.GetType() == typeof(InjectOnAttribute)) as InjectOnAttribute;
      		if (inject == null) continue;
      
      		if (injectTypes.Contains(item)) continue;
      		injectTypes.Add(item);
      
      		// 如果需要注入自身
      		if (inject.Own)
      		{
      			switch (inject.Lifetime)
      			{
      				case ServiceLifetime.Transient: services.AddTransient(item); break;
      				case ServiceLifetime.Scoped: services.AddScoped(item); break;
      				case ServiceLifetime.Singleton: services.AddSingleton(item); break;
      			}
      		}
      
      		if (inject.Scheme == InjectScheme.None) continue;
      
      		// 注入所有接口
      		if (inject.Scheme == InjectScheme.OnlyInterfaces || inject.Scheme == InjectScheme.Any)
      		{
      			var interfaces = item.GetInterfaces();
      			if (interfaces.Count() == 0) continue;
      			switch (inject.Lifetime)
      			{
      				case ServiceLifetime.Transient: interfaces.ToList().ForEach(x => services.AddTransient(x, item)); break;
      				case ServiceLifetime.Scoped: interfaces.ToList().ForEach(x => services.AddScoped(x, item)); break;
      				case ServiceLifetime.Singleton: interfaces.ToList().ForEach(x => services.AddSingleton(x, item)); break;
      			}
      		}
      
      		// 注入父類
      		if (inject.Scheme == InjectScheme.OnlyBaseClass || inject.Scheme == InjectScheme.Any)
      		{
      			var baseType = item.BaseType;
      			if (baseType == null) throw new ArgumentException($"{item.Name} 注入模式 {nameof(inject.Scheme)} 未找到父類!");
      			switch (inject.Lifetime)
      			{
      				case ServiceLifetime.Transient: services.AddTransient(baseType, item); break;
      				case ServiceLifetime.Scoped: services.AddScoped(baseType, item); break;
      				case ServiceLifetime.Singleton: services.AddSingleton(baseType, item); break;
      			}
      		}
      		if (inject.Scheme == InjectScheme.Some)
      		{
      			var types = inject.ServicesType;
      			if (types == null) throw new ArgumentException($"{item.Name} 注入模式 {nameof(inject.Scheme)} 未找到服務!");
      			switch (inject.Lifetime)
      			{
      				case ServiceLifetime.Transient: types.ToList().ForEach(x => services.AddTransient(x, item)); break;
      				case ServiceLifetime.Scoped: types.ToList().ForEach(x => services.AddScoped(x, item)); break;
      				case ServiceLifetime.Singleton: types.ToList().ForEach(x => services.AddSingleton(x, item)); break;
      			}
      		}
      	}
      }
      

      定義兩個擴展函數,用于注入入口模塊。

      		/// <summary>
      		/// 注冊模塊化服務
      		/// </summary>
      		/// <typeparam name="TModule">入口模塊</typeparam>
      		/// <param name="services"></param>
      		public static void AddModule<TModule>(this IServiceCollection services)
      			where TModule : IModule
      		{
      			AddModule(services, typeof(TModule));
      		}
      
      
      		/// <summary>
      		/// 注冊模塊化服務
      		/// </summary>
      		/// <param name="services"></param>
      		/// <param name="startupModule">入口模塊</param>
      		public static void AddModule(this IServiceCollection services, Type startupModule)
      		{
      			if (startupModule?.GetInterface(nameof(IModule)) == null)
      			{
      				throw new TypeLoadException($"{startupModule?.Name} 不是有效的模塊類");
      			}
      
      			IServiceProvider scope = BuildModule(services, startupModule);
      		}
      

      框架需要從入口模塊程序集開始查找被依賴的模塊程序集,然后通過后序遍歷初始化每個模塊,并掃描該模塊程序集中的服務。

      創建一個 BuildModule 函數,BuildModule 為構建模塊依賴樹、初始化模塊提前創建環境。

      		/// <summary>
      		/// 構建模塊依賴樹并初始化模塊
      		/// </summary>
      		/// <param name="services"></param>
      		/// <param name="startupModule"></param>
      		/// <returns></returns>
      		/// <exception cref="InvalidOperationException"></exception>
      		private static IServiceProvider BuildModule(IServiceCollection services, Type startupModule)
      		{
      			// 生成根模塊
      			ModuleNode rootTree = new ModuleNode()
      			{
      				ModuleType = startupModule,
      				Childs = new HashSet<ModuleNode>()
      			};
      
      			// 根模塊依賴的其他模塊
      			// IModule => InjectModuleAttribute
      			var rootDependencies = startupModule.GetCustomAttributes(false)
      				.Where(x => x.GetType().IsSubclassOf(typeof(InjectModuleAttribute)))
      				.OfType<InjectModuleAttribute>();
      
      			// 構建模塊依賴樹
      			BuildTree(services, rootTree, rootDependencies);
      
      			// 構建一個 Ioc 實例,以便初始化模塊類
      			var scope = services.BuildServiceProvider();
      
      			// 初始化所有模塊類
      			var serviceContext = new ServiceContext(services, scope.GetService<IConfiguration>()!);
      
      			// 記錄已經處理的程序集、模塊和服務,以免重復處理
      			HashSet<Assembly> moduleAssemblies = new HashSet<Assembly> { startupModule.Assembly };
      			HashSet<Type> moduleTypes = new HashSet<Type>();
      			HashSet<Type> injectTypes = new HashSet<Type>();
      
                  // 后序遍歷樹并初始化每個模塊
      			InitModuleTree(scope, serviceContext, moduleAssemblies, moduleTypes, injectTypes, rootTree);
      
      			return scope;
      		}
      

      第一步,構建模塊依賴樹。

      		/// <summary>
      		/// 構建模塊依賴樹
      		/// </summary>
      		/// <param name="services"></param>
      		/// <param name="currentNode"></param>
      		/// <param name="injectModules">其依賴的模塊</param>
      		private static void BuildTree(IServiceCollection services, ModuleNode currentNode, IEnumerable<InjectModuleAttribute> injectModules)
      		{
      			services.AddTransient(currentNode.ModuleType);
      			if (injectModules == null || injectModules.Count() == 0) return;
      			foreach (var childModule in injectModules)
      			{
      				var childTree = new ModuleNode
      				{
      					ModuleType = childModule.ModuleType,
      					ParentModule = currentNode
      				};
      
      				// 循環依賴檢測
      				// 檢查當前模塊(parentTree)依賴的模塊(childTree)是否在之前出現過,如果是,則說明是循環依賴
      				var isLoop = currentNode.ContainsTree(childTree);
      				if (isLoop)
      				{
      					throw new OverflowException($"檢測到循環依賴引用或重復引用!{currentNode.ModuleType.Name} 依賴的 {childModule.ModuleType.Name} 模塊在其父模塊中出現過!");
      				}
      
      				if (currentNode.Childs == null)
      				{
      					currentNode.Childs = new HashSet<ModuleNode>();
      				}
      
      				currentNode.Childs.Add(childTree);
      				// 子模塊依賴的其他模塊
      				var childDependencies = childModule.ModuleType.GetCustomAttributes(inherit: false)
      					.Where(x => x.GetType().IsSubclassOf(typeof(InjectModuleAttribute))).OfType<InjectModuleAttribute>().ToHashSet();
      				// 子模塊也依賴其他模塊
      				BuildTree(services, childTree, childDependencies);
      			}
      		}
      

      通過后序遍歷識別依賴時,由于一個模塊可能會出現多次,所以初始化時需要判斷模塊是否已經初始化,然后對模塊進行初始化并掃描模塊程序集中所有的類型,進行服務注冊。

      		/// <summary>
      		/// 從模塊樹中遍歷
      		/// </summary>
      		/// <param name="serviceProvider"></param>
      		/// <param name="context"></param>
      		/// <param name="moduleTypes">已經被注冊到容器中的模塊類</param>
      		/// <param name="moduleAssemblies">模塊類所在的程序集</param>'
      		/// <param name="injectTypes">已被注冊到容器的服務</param>
      		/// <param name="moduleNode">模塊節點</param>
      		private static void InitModuleTree(IServiceProvider serviceProvider,
      			ServiceContext context,
      			HashSet<Assembly> moduleAssemblies,
      			HashSet<Type> moduleTypes,
      			HashSet<Type> injectTypes,
      			ModuleNode moduleNode)
      		{
      			if (moduleNode.Childs != null)
      			{
      				foreach (var item in moduleNode.Childs)
      				{
      					InitModuleTree(serviceProvider, context, moduleAssemblies, moduleTypes, injectTypes, item);
      				}
      			}
      
      			// 如果模塊沒有處理過
      			if (!moduleTypes.Contains(moduleNode.ModuleType))
      			{
      				moduleTypes.Add(moduleNode.ModuleType);
      
      				// 實例化此模塊
      				// 掃描此模塊(程序集)中需要依賴注入的服務
      				var module = (IModule)serviceProvider.GetRequiredService(moduleNode.ModuleType);
      				module.ConfigureServices(context);
      				InitInjectService(context.Services, moduleNode.ModuleType.Assembly, injectTypes);
      				moduleAssemblies.Add(moduleNode.ModuleType.Assembly);
      			}
      		}
      
      

      至此,Maomi.Core 所有的代碼都已經講解完畢,通過本章的實踐,我們擁有了一個具有模塊化和自動服務注冊的框架。可是,別高興得太早,我們應當如何驗證框架是可靠的呢?答案是單元測試。在完成 Maomi.Core 項目之后,筆者立即編寫了 Maomi.Core.Tests 單元測試項目,只有當單元測試全部通過之后,筆者才能自信地把代碼放到書中。為項目編寫單元測試是一個好習慣,尤其是對框架類的項目,我們需要編寫大量的單元測試驗證框架的可靠性,同時單元測試中大量的示例是其他開發者了解框架、入手框架的極佳參考。

      發布到 nuget

      們開發了一個支持模塊化和自動服務注冊的框架,通過 Maomi.Core 實現模塊化應用的開發。

      完成代碼后,我們需要將代碼共享給其他人,那么可以使用 nuget 包的方式。

      當類庫開發完成后,我們可以打包成 nuget 文件,上傳這個到 nuget.org ,或者是內部的私有倉庫,供其他開發者使用。

      Maomi.Core.csproj 項目的的 PropertyGroup 屬性中加上以下配置,以便能夠在發布類庫時,生成 nuget 包。

      		<IsPackable>true</IsPackable>
      		<PackageVersion>1.0.0</PackageVersion>
      		<Title>貓咪框架</Title>
      		<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
      

      或者右鍵點擊項目-屬性-打包。

      image-20230725193431285

      當然,你也可以在 Visual Studio 中點擊項目右鍵屬性,在面板中進行可視化配置。

      image-20230227190412986

      你可以配置項目的 github 地址、發布說明、開源許可證等。

      配置完成后,可以使用 Visual Studio 發布項目,或使用 dotnet publish -c Release 命令發布項目。

      image-20230227190526152

      發布項目后,可以在輸出目錄找到 .nupkg 文件。

      image-20230227190731544

      打開 https://www.nuget.org/packages/manage/upload ,登錄后上傳 .nupkg 文件。

      image-20230227191132125

      制作模板項目

      在 .NET 中,安裝 .NET SDK 時默認攜帶了一些項目模板,使用 dotnet new list 可以看到本機中已經按照的項目模板,然后通過dotnet new {模板名稱} 命令使用模板快速創建一個應用。

      通過模板創建一個應用是很方便的,項目模板提前組織好解決方案中的項目結構、代碼文件,開發者使用模板時只需要提供一個名稱,然后即可生成一個完整的應用。那么在本節中,筆者將會介紹如何制作自己的項目模板,進一步打包到 nuget 中,分享給更多的開發者使用。當然,在企業開發中,架構師可以規劃好基礎代碼、設計項目架構,然后制作模板項目,業務開發者需要創建新的項目時,從企業基礎項目模板一鍵生成即可,從而可以快速開發項目。

      本節的示例代碼在 demo/1/templates 中。

      讓我們來體驗筆者已經制作好的項目模板,執行以下命令從 nuget 中安裝模板。

      dotnet new install Maomi.Console.Templates::2.0.0
      

      命令執行完畢后,控制臺會打?。?/p>

      模板名        短名稱  語言  標記
      ------------  ------  ----  --------------
      Maomi 控制臺  maomi   [C#]  Common/Console
      

      使用模板名稱 maomi 創建自定義名稱的項目:

       dotnet new maomi --name MyTest
      

      image-20231219190033967

      打開 Visual Studio,可以看到最近通過 nuget 安裝的模板。

      image-20231219190242308

      接下來,我們來上手制作一個屬于自己的模板。

      打開 demo/1/templates 目錄,可以看到文件組織如下所示:

      .
      │  MaomiPack.csproj
      │
      └─templates
          │  Maomi.Console.sln
          │  template.json
          │
          ├─Maomi.Console
          │      ConsoleModule.cs
          │      Maomi.Console.csproj
          │      Program.cs
          │
          └─Maomi.Lib
                  IMyService.cs
                  LibModule.cs
                  Maomi.Lib.csproj
                  MyService.cs
      

      創建 MaomiPack.csproj 文件(名稱可以自定義),該文件用于將代碼打包到 nuget 包中,否則 dotnet cli 會先編譯項目再打包到 nuget 包中。

      <Project Sdk="Microsoft.NET.Sdk">
        <PropertyGroup>
          <PackageType>Template</PackageType>
          <PackageVersion>2.0.0</PackageVersion>
          <PackageId>Maomi.Console.Templates</PackageId>
          <PackageTags>dotnet-new;templates;contoso</PackageTags>
      
          <Title>Maomi 框架控制臺模板</Title>
          <Authors>癡者工良</Authors>
          <Description>用于示范 Maomi 框架的模板項目包.</Description>
      
          <TargetFramework>net8.0</TargetFramework>
         
          <IncludeContentInPack>true</IncludeContentInPack>
          <IncludeBuildOutput>false</IncludeBuildOutput>
          <ContentTargetFolders>content</ContentTargetFolders>
          <NoWarn>$(NoWarn);NU5128</NoWarn>
        </PropertyGroup>
      
        <ItemGroup>
          <Content Include="templates\**\*" Exclude="templates\**\bin\**;templates\**\obj\**" />
          <Compile Remove="**\*" />
        </ItemGroup>
      
      </Project>
      
      • PackageVersion :模板版本號。
      • PackageId:模板 id,在 nuget.org 中唯一。
      • PackageTags:nuget 包的標記。
      • Title:nuget 包標題。
      • Authors:作者名稱。
      • Description:nuget 包描述。

      創建一個空目錄存儲項目代碼,一般使用 templates 命名,你可以參考 demo/1/templates/templates 中的解決方案。接著在該目錄下創建 template.json 文件,文件內容如下:

      {
        "$schema": "http://json.schemastore.org/template",
        "author": "癡者工良",
        "classifications": [
          "Common",
          "Console"
        ],
        "identity": "Maomi.Console",
        "name": "Maomi 控制臺",
        "description": "這是一個使用 Maomi.Core 搭建的模塊化應用模板。",
        "shortName": "maomi",
        "tags": {
          "language": "C#",
          "type": "project"
        },
        "sourceName": "Maomi",
        "preferNameDirectory": true
      }
      

      template.json 文件用于配置項目模板屬性,在安裝模板后相關信息會顯示到 Visual Studio 項目模板列表,以及創建項目時自動替換 Maomi 前綴為自定義的名稱。

      • author:作者名稱。
      • classifications:項目類型,如控制臺、Web、Wpf 等。
      • identity:模板唯一標識。
      • name:模板名稱。
      • description模板描述信息。
      • shortName:縮寫,使用 dotnet new {shortName} 命令時可以簡化模板名稱。
      • tags:指定了模板使用的語言和項目類型。
      • sourceName:可以被替換的名稱,例如 Maomi.Console 將會被替換為 MyTest.Console,模板中所有文件名稱、字符串內容都會被替換。

      組織好模板之后,在 MaomiPack.csproj 所在目錄下執行 dotnet pack 命令打包項目為 nuget 包,最后根據提示生成的 nuget 文件,上傳到 nuget.org 即可。

      posted @ 2024-06-03 08:30  癡者工良  閱讀(1313)  評論(4)    收藏  舉報
      主站蜘蛛池模板: 日本丶国产丶欧美色综合| 国产福利深夜在线观看| 亚洲第一区二区快射影院| 波多野结衣久久一区二区| 熟妇人妻无码中文字幕老熟妇| 国产成人午夜精品影院| 久久青青草原亚洲AV无码麻豆| 肥臀浪妇太爽了快点再快点| 激情综合色综合久久综合| 亚洲男人在线天堂| 免费久久人人香蕉av| 国精偷拍一区二区三区| 好紧好湿太硬了我太爽了视频| 国产成人精品亚洲高清在线| 国产精品久久蜜臀av| 中国熟女仑乱hd| 亚洲人黑人一区二区三区| 无码欧亚熟妇人妻AV在线外遇| 国产亚洲精品在av| 国产精品大全中文字幕| 中文无码乱人伦中文视频在线| 国产在线精彩自拍视频| 色欲av无码一区二区人妻| 日韩激情无码av一区二区| 成在线人视频免费视频| 国产精品制服丝袜第一页| 欧美乱妇高清无乱码免费| 潢川县| 中文字幕日韩精品东京热| 337p粉嫩大胆噜噜噜| 国语精品国内自产视频| 精品久久久久中文字幕APP| 免费久久人人爽人人爽AV| 麻豆精品一区二区三区蜜桃| 中文人妻AV高清一区二区| 吉川爱美一区二区三区视频| 国产美女白丝袜精品_a不卡| 亚洲自拍偷拍一区二区三区| 国产日韩久久免费影院| 精品国产高清中文字幕| 欧洲无码一区二区三区在线观看|