我們一起完成插件框架的設計與實現

来源:https://www.cnblogs.com/adair-blog/archive/2019/04/21/10743701.html
-Advertisement-
Play Games

開場一些題外話,今天登陸這個"小菜"的博客園,感觸頗多。"小菜"是我以前在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))
                  .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); } }
從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這個文件,看代碼。      
   /// <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的定義
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 最近有一個雲伺服器和資料庫的遷移任務,踩坑爬坑無數次,覺得必須要記錄一下。大家瓜子花生準備好,聽我慢慢講故事#手動笑哭#。 故事背景 公司是做電商業務的,在天貓有幾家旗艦店數據量也很大。阿裡有一個稱為聚石塔的平臺,專門給這些ISV提供各種雲資源,強制綁定了一些業務,原本我們在聚石塔中有一臺ECS和一 ...
  • 一.StackExchange.Redis StackExchange.Redis是由Stack Overflow開發的C#語言Redis客戶端,使用廣泛,本文針對 StackExchange.Redis 進一步擴展使之支持實體 二. 使用Demo 1. 安裝 2. Demo 移步我的項目https ...
  • 建造者模式簡介 建造者模式又稱生成器模式,它將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。這就要求,我們所要處理的對象必須要有非常強的結構化特征,對於開發人員而言,需要抽象出一般的結構化介面出來,方便進行構建工作。 作為一名碼農,最難得的應該就是找對象了,就算勉強找到了, ...
  • 又是一年金三銀四,應該不少小伙伴開始摩拳擦掌準備換個好工作了吧?想要在尋找工作中突出自我,穩穩在大浪淘沙的職場中立足,掌握一些專業工具是很有必要的,今天要給職場人分享投遞簡歷如何選擇安全、便捷的電子郵箱三大法則,作為能力不差、想要變成更好自己的你,值得花2分鐘瞭解一下。 電子郵件的安全性為何如此重要 ...
  • WPF窗體中建有一個DataGrid,運行後修改各行數據,通過Update方法更新後臺資料庫。發現在資料庫中,其中一列FAcctID(文本型)每次都會變為0,還有一列FDebit(貨幣型)不能更新,其他列則沒有問題。主要代碼如下: 排查了好幾個小時,對Xaml代碼綁定、Parameter參數進行修改 ...
  • 在平常得編程中,經常會用到觀察者模式,屬於使用場景很頻繁得設計模式之一。 學習設計模式得目的,知道在何種場景中使用合適得設計模式。 以下我將從現實得場景出發去講設計模式得運用,從比較中去看出優勢 1.觀察者模式得定義 目標狀態發生變化得時候把他得信息通知給註冊過得觀察者們。 當一個對象改變需要同時通 ...
  • 問題描述 在傳統的基於 .NET Framework 的 WPF 程式中,我們可以使用如下代碼段啟動相關的預設應用: 但是上述協議方式在 .NET Core 中不再適用,當我們使用上述方式進行操作,程式會給我們爆如下的錯誤: 經 "神樹桜乃" 大佬提醒,我特意看了一下 ProcessStartInf ...
  • 20190421asp.netajax與jquery和bootstrap的無刷新完美實現 設計代碼和後臺代碼中重要部分加粗和深色以及字型大小加大。 設計前臺代碼: <%@ Page Title="添加/修改商家" Language="C#" MasterPageFile="~/Site_cg007.Ma ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...