開場一些題外話,今天登陸這個"小菜"的博客園,感觸頗多。"小菜"是我以前在QQ群裡面的網名,同時也申請了這個博客園賬戶,五年前的"小菜"在NET和C++某兩個群裡面非常的活躍,也非常熱心的幫助網友盡能力所及解決技術上的問題。依稀記得當時NET群裡面的"青菊、Allen、酷酷",C++群裡面的"夏老師 ...
開場一些題外話,今天登陸這個"小菜"的博客園,感觸頗多。"小菜"是我以前在QQ群裡面的網名,同時也申請了這個博客園賬戶,五年前的"小菜"在NET和C++某兩個群裡面非常的活躍,也非常熱心的幫助網友盡能力所及解決技術上的問題。依稀記得當時NET群裡面的"青菊、Allen、酷酷",C++群裡面的"夏老師、風箏兄"等網友、哥們。時過境遷,後來因為某些原因而慢慢淡出了QQ群里的技術交流,在這裡我真的非常感謝網友"於兄"推薦我到北京某家公司上班,也很懷念當年無話不談的網友們。 題外話有點多啊,希望理解,直接進入主題。本人陸續寫過三個WEB版的插件式框架,有基於WEBFORM平臺、ASPNETMVC平臺、ASPNETMVCCORE平臺。今天給大家分享的是以前在工作中自己負責的一個基於ASPNETMVC平臺的WEB插件框架"Antiquated"取名叫"過時的",過時是因為現在NETCORE正大行其道。 插播一個小廣告,有興趣的朋友可以看看,htttp://www.xinshijie.store. 正式進入主題之前,我想大家先看看效果,由於是圖片錄製,我就隨便點擊錄製了一下。 插件框架 插件我個人的理解為大到模塊小到方法甚至一個頁面的局部顯示都可視為一個獨立的插件。站在開發者的角度來說,結構清晰、獨立、耦合度低、易維護等特點,而且可實現熱插拔。當然對於插件小到方法或者局部顯示的這個理念的認知也是在接觸NOP之後才有的,因為在此之前基於WEBFORM平臺實現的插件框架僅僅是按模塊為單位實現的插件框架。以上僅是我個人理解,不喜勿噴。 框架 (framework)是一個框子——指其約束性,也是一個架子——指其支撐性。是一個基本概念上的結構,用於去解決或者處理複雜的問題,這是百度百科的定義。通俗的講,框架就是一個基礎結構,比如建築行業,小區的設計,房屋的地基結構等。IT行業軟體系統也類似,框架承載了安全、穩定性、合理性等等特點,一個好的基礎框架應該具有以上特點。本文的意圖是跟大家一起討論一個框架的實現思路,並不是去深入的研究某個技術點。 實現思路 應用框架,設計的合理性我覺得比設計本身重要,本人接觸過多個行業,看到過一些內部開發框架,為了設計而過於臃腫。本人以前寫過通信類的框架,如果你完全採用OO的設計,那你會損失不少性能上的問題。言歸正傳,插件應用框架我們可以理解為一個應用框架上面承載了多種形式上的獨立插件的熱插拔。應用框架你最好有緩存,我們可以理解為一級緩存、日誌、認證授權、任務管理、文件系統等等基礎功能並且自身提供相關預設實現,對於後期的定製也應該能夠輕鬆的實現相關功能點的適配能力。應用框架也並不是所謂的完全是從無到有,我們可以根據業務需求,人力資源去選擇合適的WEB平臺加以定製。微軟官方的所有WEB平臺都是極具擴展的基礎平臺,統一的管道式設計,讓我們可以多維度的切入和定製。作為一個應用框架肯定也會涉及大量的實體操作對象,這時候我們可能會遇到幾個問題,實體的創建和生命周期的管理。如果我們採用原始的New操作,即便你能把所有創建型設計模式玩的很熟,那也是一件比較頭痛的事。對於MVC架構模式下的特殊框架ASPNETMVC而言,之所以用"特殊"這個詞加以修飾,是因為ASPNETMVC應該是基於一個變體的MVC架構實現,其中的Model也僅僅是ViewModel,所以我們需要在領域模型Model與ViewModel之間做映射。以上是個人在工作中分析問題的一些經驗和看法,如有不對,見諒! "Antiquated"插件框架參考NOP、KIGG等開源項目,根據以上思路分析使用的技術有:MVC5+EF6+AUTOMAPPER+AUTOFAC+Autofac.Integration.Mvc+EnterpriseLibrary等技術, 算是一個比較常見或者相對標準的組合吧,Antiquated支持多主題、多語言、系統設置、角色許可權、日誌等等功能。 項目目錄結構 項目目錄結構採用的是比較經典的"三層結構",此三層非彼三層,當然我是以文件目錄劃分啊。分為基礎設施層(Infrastructures)、插件層(Plugins)、表示層(UI),看圖 目錄解說: Infrastructures包含Core、Database、Services、PublicLibrary三個工程,其關聯關係類似於"適配"的一種關係,也可理解為設計模式裡面的適配器模式。Core裡面主要是整個項目的基礎支撐組件、預設實現、以及領域對象"規約"。 SQLDataBase為EF For SqlServer。Services為領域對象服務。PublicLibrary主要是日誌、緩存、IOC等基礎功能的預設實現。 Plugins文件夾包含所有獨立插件,Test1為頁面插件,顯示到頁面某個區域。Test2為Fun插件裡面僅包含一個獲取數據的方法。 UI包括前臺展示和後臺管理 Framwork文件夾主要是ASPNETMVC基礎框架擴展。說了這麼多白話,接下來我們具體看看代碼的實現和效果。 整個應用框架我重點解說兩個部分基礎部分功能和插件。我們先看入口Global.asax,一下關於代碼的說明,我只挑一些重要的代碼加以分析說明,相關的文字註釋也做的比較詳細,代碼也比較簡單明瞭,請看代碼 基礎部分
protected void Application_Start() { // Engine初始化 EngineContext.Initialize(DataSettingsHelper.DatabaseIsInstalled()); // 添加自定義模型綁定 ModelBinders.Binders.Add(typeof(BaseModel), new AntiquatedModelBinder()); if (DataSettingsHelper.DatabaseIsInstalled()) { // 清空mvc所有viewengines ViewEngines.Engines.Clear(); // 註冊自定義mvc viewengines ViewEngines.Engines.Add(new ThemableRazorViewEngine()); } // 自定義元數據驗證 ModelMetadataProviders.Current = new AntiquatedMetadataProvider(); AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); DataAnnotationsModelValidatorProvider .AddImplicitRequiredAttributeForValueTypes = false; // 註冊模型驗證 ModelValidatorProviders.Providers.Add( new FluentValidationModelValidatorProvider(new AntiquatedValidatorFactory())); // 註冊虛擬資源提供程式 var viewResolver = EngineContext.Current.Resolve<IAntiquatedViewResolver>(); var viewProvider = new ViewVirtualPathProvider(viewResolver.GetEmbeddedViews()); HostingEnvironment.RegisterVirtualPathProvider(viewProvider); }我們往往在做系統或者應用框架開發的時候,一般會去找基礎框架給我們提供的合適切入點實現全局初始化。相信玩ASP.NET的朋友應該對Global.asax這個cs文件比較熟悉,或者說他的基類HttpApplication,大概說一下這個HttpApplication對象,HttpApplication的創建和處理時機是在運行時HttpRuntime之後,再往前一點就是IIS伺服器容器了,所以HttpApplication就是我們要找的切入點。 EngineContext初看著命名挺唬人的,哈哈,其實還是比較簡單的一個對象,我們暫時管它叫"核心對象上下文"吧,個人的一點小建議,我們在做應用框架的時候,最好能有這麼一個核心對象來管理所有基礎對象的生命周期。先上代碼
/// <summary> /// 初始化engine核心對象 /// </summary> /// <returns></returns> [MethodImpl(MethodImplOptions.Synchronized)] public static IEngine Initialize(bool databaseIsInstalled) { if (Singleton<IEngine>.Instance == null) { var config = ConfigurationManager.GetSection("AntiquatedConfig") as AntiquatedConfig; Singleton<IEngine>.Instance = CreateEngineInstance(config); Singleton<IEngine>.Instance.Initialize(config, databaseIsInstalled); } return Singleton<IEngine>.Instance; }它的職責還是比較簡單,以單例模式線程安全的形式負責創建和初始化核心對象Engine,當然它還有第二個職責封裝Engine核心對象,看代碼
public static IEngine Current { get { if (Singleton<IEngine>.Instance == null) { Initialize(true); } return Singleton<IEngine>.Instance; } }麻煩大家註意一個小小的細節,EngineContext-Engine這兩個對象的命名,xxxContext某某對象的上下文(暫且這麼翻譯吧,因為大家都這麼叫)。我們閱讀微軟開源源碼比如ASPNETMVC WEBAPI等等,經常會碰到這類型的命名。個人理解, Context是對邏輯業務範圍的劃分、對象管理和數據共用。我們接著往下看,Engine裡面到底做了哪些事情,初始化了哪些對象,上代碼。
/// <summary> /// IEngine /// </summary> public interface IEngine { /// <summary> /// ioc容器 /// </summary> IDependencyResolver ContainerManager { get; } /// <summary> /// engine初始化 /// </summary> /// <param name="config">engine配置</param> /// <param name="databaseIsInstalled">資料庫初始化</param> void Initialize(AntiquatedConfig config, bool databaseIsInstalled); /// <summary> /// 反轉對象-泛型 /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> T Resolve<T>() where T : class; /// <summary> /// 反轉對象 /// </summary> /// <param name="type"></param> /// <returns></returns> object Resolve(Type type); IEnumerable<T> ResolveAll<T>(); }其一初始化IDependencyResolver容器,這個IDependencyResolver非MVC框架裡面的內置容器,而是我們自定義的容器介面,我們後續會看到。其二基礎對象全局配置初始化。 其三後臺任務執行。其四提供容器反轉對外介面,當然這個地方我也有那麼一點矛盾,是不是應該放在這個地方,而是由IOC容器自己來對外提供更好呢?不得而知,暫且就這麼做吧。看到這裡,我們把這個對象取名為engine核心對象應該還是比較合適吧。 下麵我們重點看看IDependencyResolver容器和任務Task
/// <summary> /// ioc容器介面 /// </summary> public interface IDependencyResolver : IDisposable { /// <summary> /// 反轉對象 /// </summary> /// <param name="type"></param> /// <returns></returns> object Resolve(Type type); object ResolveUnregistered(Type type); void RegisterAll(); void RegisterComponent(); void Register<T>(T instance, string key) where T:class; /// <summary> /// 註入對象 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="existing"></param> void Inject<T>(T existing); T Resolve<T>(Type type) where T:class; T Resolve<T>(Type type, string name); bool TryResolve(Type type, out object instance); T Resolve<T>(string key="") where T:class; IEnumerable<T> ResolveAll<T>(); }容器介面本身的功能沒有過多要說的,都是一些標準的操作,玩過容器的應該都比較熟悉。接下來我們重點看看容器的創建和適配。容器的創建交由IDependencyResolverFactory工廠負責創建,IDependencyResolverFactory介面定義如下
public interface IDependencyResolverFactory { IDependencyResolver CreateInstance(); }IDependencyResolverFactory工廠就一個方法創建容器,由它的實現類DependencyResolverFactory實現具體的對象創建,看代碼
public class DependencyResolverFactory : IDependencyResolverFactory { private readonly Type _resolverType; public DependencyResolverFactory(string resolverTypeName) { _resolverType = Type.GetType(resolverTypeName, true, true); } // 從配置文件獲取ioc容器類型 public DependencyResolverFactory() : this(new ConfigurationManagerWrapper().AppSettings["dependencyResolverTypeName"]) { } // 反射創建容器對象 public IDependencyResolver CreateInstance() { return Activator.CreateInstance(_resolverType) as IDependencyResolver; } }<add key="dependencyResolverTypeName" value="Antiquated.PublicLibrary.AutoFac.AutoFacDependencyResolver, Antiquated.PublicLibrary"/>我把配置節點也一併貼出來了,代碼邏輯也比較簡單,一看就明白了,整個創建過程算是基於一個標準的工廠模式實現,通過反射實現容器對象創建。接下來我們看看創建出來的具體ioc容器DefaultFacDependencyResolver,看代碼。
public class DefaultFacDependencyResolver : DisposableResource, Core.Ioc.IDependencyResolver, // 這就是我們上面貼出來的容器介面 IDependencyResolverMvc // MVC內置容器介面對象,實現mvc全局容器註入 { // autofac容器 private IContainer _container; public IContainer Container { get { return _container; } } public System.Web.Mvc.IDependencyResolver dependencyResolverMvc { get => new AutofacDependencyResolver(_container); } public DefaultFacDependencyResolver() : this(new ContainerBuilder()) { } public DefaultFacDependencyResolver(ContainerBuilder containerBuilder) { // build容器對象 _container = containerBuilder.Build(); } // ...... 此處省略其他代碼 }DefaultFacDependencyResolver顧名思義就是我們這個應用框架的預設容器對象,也就是上面說的應用框架最好能有一套基礎功能的預設實現,同時也能輕鬆適配新的功能組件。比如,我們現在的預設IOC容器是Autofac,當然這個容器目前來說還 是比較不錯的選擇,輕量級,高性能等。假如哪天Autofac不再更新,或者有更好或者更適合的IOC容器,根據開閉原則,我們就可以輕鬆適配新的IOC容器,降低維護成本。對於IOC容器的整條管線差不多就已經說完,下麵我們看看任務 IBootstrapperTask的定義。
/// <summary> /// 後臺任務 /// </summary> public interface IBootstrapperTask { /// <summary> /// 執行任務 /// </summary> void Execute(); /// <summary> /// 任務排序 /// </summary> int Order { get; } }IBootstrapperTask的定義很簡單,一個Execute方法和一個Order排序屬性,接下來我們具體看看後臺任務在IEngine裡面的執行機制。
public class Engine : IEngine { public void Initialize(AntiquatedConfig config, bool databaseIsInstalled) { // 省略其他成員... ResolveAll<IBootstrapperTask>().ForEach(t => t.Execute()); } // ...... 此處省略其他代碼 }代碼簡單明瞭,通過預設容器獲取所有實現過IBootstrapperTask介面的任務類,執行Execute方法,實現後臺任務執行初始化操作。那麼哪些功能可以實現在後臺任務邏輯裡面呢?當然這個也沒有相應的界定標準啊,我的理解一般都是一些公共的 基礎功能,需要提供一些基礎數據或者初始化操作。比如郵件、預設用戶數據等等。比如我們這個應用框架其中就有一個後臺任務Automapper的映射初始化操作,看代碼
public class AutoMapperStartupTask : IBootstrapperTask { public void Execute() { if (!DataSettingsHelper.DatabaseIsInstalled()) return; Mapper.CreateMap<Log, LogModel>(); Mapper.CreateMap<LogModel, Log>() .ForMember(dest => dest.CreatedOnUtc, dt => dt.Ignore()); // ...... 此處省略其他代碼 } }到此基礎部分我挑選出了Engine、ioc、task這幾部分大概已經說完當然Engine還包括其他一些內容,比如緩存、日誌、全局配置、文件系統、認證授權等等。由於時間篇幅的問題,我就不一一介紹了。既然是插件應用框架,那肯定就少不了插件的 講解,下麵我們繼續講解第二大部分,插件。 插件部分 IPlugin插件介面定義如下
/// <summary> /// 插件 /// </summary> public interface IPlugin { /// <summary> /// 插件描述對象 /// </summary> PluginDescriptor PluginDescriptor { get; set; } /// <summary> /// 安裝插件 /// </summary> void Install(); /// <summary> /// 卸載插件 /// </summary> void Uninstall(); }IPlugin插件介面包含三個成員,一個屬性插件描述對象,和安裝卸載兩個方法。安裝卸載方法很好理解,下麵我們看看PluginDescriptor的定義
/// <summary> /// 插件描述對象 /// </summary> public class PluginDescriptor : IComparable<PluginDescriptor> { public PluginDescriptor() { } /// <summary> /// 插件dll文件名稱 /// </summary> public virtual string PluginFileName { get; set; } /// <summary> /// 類型 /// </summary> public virtual Type PluginType { get; set; } /// <summary> /// 插件歸屬組 /// </summary> public virtual string Group { get; set; } /// <summary> /// 別名,友好名稱 /// </summary> public virtual string FriendlyName { get; set; } /// <summary> /// 插件系統名稱,別名的一種 /// </summary> public virtual string SystemName { get; set; } /// <summary> /// 插件版本 /// </summary> public virtual string Version { get; set; } /// <summary> /// 插件作者 /// </summary> public virtual string Author { get; set; } /// <summary> /// 顯示順序 /// </summary> public virtual int DisplayOrder { get; set; } /// <summary> /// 是否安裝 /// </summary> public virtual bool Installed { get; set; } // 省略其他代碼... }從PluginDescriptor的定義,我們瞭解到就是針對插件信息的一些描述。對於插件應用框架,會涉及到大量的插件,那麼我們又是如果管理這些插件呢?我們接著往下看,插件管理對象PluginManager。
// 程式集載入時自執行 [assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")] namespace Antiquated.Core.Plugins { /// <summary> /// 插件管理 /// </summary> public class PluginManager { // ...... 此處省略其他代碼 private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); private static readonly string _pluginsPath = "~/Plugins"; /// <summary> /// 插件管理初始化操作 /// </summary> public static void Initialize() { using (new WriteLockDisposable(Locker)) { try { // ...... 此處省略其他代碼 // 載入所有插件描述文件 foreach (var describeFile in pluginFolder.GetFiles("PluginDescribe.txt", SearchOption.AllDirectories)) { try { // 解析PluginDescribe.txt文件獲取describe描述對象 var describe = ParsePlugindescribeFile(describeFile.FullName); if (describe == null) continue; // 解析插件是否已安裝 describe.Installed = installedPluginSystemNames .ToList() .Where(x => x.Equals(describe.SystemName, StringComparison.InvariantCultureIgnoreCase)) .FirstOrDefault() != null; // 獲取所有插件dll文件 var pluginFiles = describeFile.Directory.GetFiles("*.dll", SearchOption.AllDirectories) .Where(x => !binFiles.Select(q => q.FullName).Contains(x.FullName)) .Where(x => IsPackagePluginFolder(x.Directory)) .ToList(); //解析插件dll主程式集 var mainPluginFile = pluginFiles.Where(x => x.Name.Equals(describe.PluginFileName, StringComparison.InvariantCultureIgnoreCase))從PluginManager的部分代碼實現來看,它主要做了這麼幾件事,1:載入所有插件程式集,:2:解析所有插件程式集並初始化,:3:添加程式集引用到應用程式域,4:寫入插件文件信息,最後負責插件的安裝和卸載。以上就是插件管理的部分核心代碼,代碼註釋也比較詳細,大家可以稍微花點時間看下代碼,整理一下實現邏輯。麻煩大家註意一下中間標紅的幾處代碼,這也是實現插件功能比較容易出問題的幾個地方。首先我們看到這行代碼[assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")],這是ASP.NET4.0及以上版本新增的擴展點,其作用有兩點,其一配合BuildManager.AddReferencedAssembly()實現動態添加外部程式集的依賴,其二可以讓我們的Initialize插件初始化函數執行在我們的Global.asax的Application_Start()方法之前,因為微軟官方描述BuildManager.AddReferencedAssembly方法必須執行在Application_Start方法之前。最後還有一個需要註意的小地方,有些朋友可能想把插件副本文件複製到 應用程式域的DynamicDirectory目錄,也就是ASP.NET的編譯目錄,如果是複製到這個目錄的話,一定要註意許可權問題,CLR代碼訪問安全(CAS)的問題。CAS代碼訪問安全是CLR層面的東西,有興趣的朋友可以去瞭解一下,它可以幫助我們在日後的開發中解決不少奇葩問題。 插件業務邏輯實現 首先聲明,MVC實現插件功能的方式有很多種,甚至我一下要講解的這種還算是比較麻煩的,我之所以選擇一下這種講解,是為了讓我們更全面的瞭解微軟的web平臺,以及ASPNETMVC框架內部本身。後續我也會稍微講解另外一種比較簡單的實現方式。我們繼續,讓我們暫時先把視線轉移到Global.asax這個文件,看代碼。
.FirstOrDefault(); describe.OriginalAssemblyFile = mainPluginFile; // 添加插件程式集引用 foreach (var plugin in pluginFiles.Where(x => !x.Name.Equals(mainPluginFile.Name, StringComparison.InvariantCultureIgnoreCase))) PluginFileDeploy(plugin); // ...... 此處省略其他代碼 } catch (Exception ex) { thrownew Exception("Could not initialise plugin folder", ex);; } } } catch (Exception ex) { thrownew Exception("Could not initialise plugin folder", ex);; } } } /// <summary> /// 插件文件副本部署並添加到應用程式域 /// </summary> /// <param name="plug"></param> /// <returns></returns> private static Assembly PluginFileDeploy(FileInfo plug) { if (plug.Directory.Parent == null) throw new InvalidOperationException("The plugin directory for the " + plug.Name + " file exists in a folder outside of the allowed Umbraco folder heirarchy"); FileInfo restrictedPlug; var restrictedTempCopyPlugFolder= Directory.CreateDirectory(_restrictedCopyFolder.FullName); // copy移動插件文件到指定的文件夾 restrictedPlug = InitializePluginDirectory(plug, restrictedTempCopyPlugFolder); // 此處省略代碼... var restrictedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(restrictedPlug.FullName)); BuildManager.AddReferencedAssembly(restrictedAssembly); return restrictedAssembly; } /// <summary> /// 插件安裝 /// </summary> /// <param name="systemName"></param> public static void Installed(string systemName) { // 此處省略其他代碼.... // 獲取所有已安裝插件 var installedPluginSystemNames = InstalledPluginsFile(); // 獲取當前插件的安裝狀態 bool markedInstalled = installedPluginSystemNames .ToList() .Where(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) .FirstOrDefault() != null; // 如果當前插件狀態為未安裝狀態,添加到待安裝列表 if (!markedInstalled) installedPluginSystemNames.Add(systemName); var text = MergeInstalledPluginsFile(installedPluginSystemNames); // 寫入文件 File.WriteAllText(filePath, text); } /// <summary> /// 插件卸載 /// </summary> /// <param name="systemName"></param> public static void Uninstalled(string systemName) { // 此處省略其他代碼.... // 邏輯同上 File.WriteAllText(filePath, text); } }
/// <summary> /// 系統初始化 /// </summary> protected void Application_Start() { // 此處省略其他代碼... // 註冊虛擬資源提供程式 var viewResolver = EngineContext.Current.Resolve<IAntiquatedViewResolver>(); var viewProvider = new ViewVirtualPathProvider(viewResolver.GetEmbeddedViews()); //註冊 HostingEnvironment.RegisterVirtualPathProvider(viewProvider); }通過EngineContext上下文對象獲取一個IAntiquatedViewResolver對象,IAntiquatedViewResolver這個對象到底是什麼?怎麼定義的?我們繼續往下看。
public interface IAntiquatedViewResolver { EmbeddedViewList GetEmbeddedViews(); }IAntiquatedViewResolver裡面就定義了一個方法,按字面意思的理解就是獲取所有嵌入的views視圖資源,沒錯,其實它就是乾這件事的。是不是覺得插件的實現是不是有點眉目了?呵呵。不要急,我們接著往下看第二個對象ViewVirtualPathProvider對象。
/// <summary> /// 虛擬資源提供者 /// </summary> public class ViewVirtualPathProvider : VirtualPathProvider { /// <summary> /// 嵌入的視圖資源列表 /// </summary> private readonly EmbeddedViewList _embeddedViews; /// <summary> /// 對象初始化 /// </summary> /// <param name="embeddedViews"></param> public ViewVirtualPathProvider(EmbeddedViewList embeddedViews) { if (embeddedViews == null) throw new ArgumentNullException("embeddedViews"); this._embeddedViews = embeddedViews; } /// <summary> /// 重寫基類FileExists /// </summary> /// <param name="virtualPath"></param> /// <returns></returns> public override bool FileExists(string virtualPath) { // 如果虛擬路徑文件存在 return (IsEmbeddedView(virtualPath) || Previous.FileExists(virtualPath)); } /// <summary> /// 重寫基類GetFile /// </summary> /// <param name="virtualPath"></param> /// <returns></returns> public override VirtualFile GetFile(string virtualPath) { // 判斷是否為虛擬視圖資源 if (IsEmbeddedView(virtualPath)) { // 部分代碼省略... // 獲取虛擬資源 return new EmbeddedResourceVirtualFile(embeddedViewMetadata, virtualPath); } return Previous.GetFile(virtualPath); } }定義在ViewVirtualPathProvider中的成員比較核心的就是一個列表和兩個方法,這兩個方法不是它自己定義,是重寫的VirtualPathProvider基類裡面的方法。我覺得ViewVirtualPathProvider本身的定義和邏輯都很簡單,但是為了我們能更好的理解這麼一個虛擬資源對象,我們很有必要瞭解一下它的基類,虛擬資源提供程式VirtualPathProvider這個對象。 VirtualPathProvider虛擬資源提供程式,MSDN上的描述是,提供了一組方法,使 Web 應用程式可以從虛擬文件系統中檢索資源,所屬程式集是System.Web。System.Web這個大小通吃的程式集除開ASP.NETCORE,之前微軟所有的WEB開發平臺都能看到它神一樣的存在。吐槽了一下System.Web,我們接著說VirtualPathProvider對象。
public abstract class VirtualPathProvider : MarshalByRefObject { // 省略其他代碼... protected internal VirtualPathProvider Previous { get; } public virtual bool FileExists(string virtualPath); public virtual VirtualFile GetFile(string virtualPath); }從VirtualPathProvider對象的定義來看,它是跟文件資源相關的。WEBFORM平臺的請求資源對應的是伺服器根目錄下麵的物理文件,沒有就會NotFound。如果我們想從資料庫或者依賴程式集的嵌入的資源等地方獲取資源呢?沒關係VirtualPathProvider可以幫我解決。VirtualPathProvider派生類ViewVirtualPathProvider通過Global.asax的HostingEnvironment.RegisterVirtualPathProvider(viewProvider)實現註冊,所有的請求資源都必須經過它,所以我們的插件程式集嵌入的View視圖資源的處理,只需要實現兩個邏輯FileExists和GetFile。我們不防再看一下ViewVirtualPathProvider實現類的這兩個邏輯,如果是嵌入的資源,就實現我們自己的GetFile邏輯,讀取插件視圖文件流。否則交給系統預設處理。 說到這裡,可能有些朋友對於FileExists和GetFile的執行機制還是比較困惑,好吧,索性我就一併大概介紹一下吧,刨根問底是我的性格,呵呵。需要描述清楚這個問題,我們需要關聯到我們自定義的AntiquatedVirtualPathProviderViewEngine的實現,AntiquatedVirtualPathProviderViewEngine繼承自VirtualPathProviderViewEngine,我們先來看下VirtualPathProviderViewEngine的定義