Implementing Plug-in Framework from an Analytical Perspective

Keywords: ASP.NET Attribute Database IIS Web Development

 
Opening some off-topic remarks, today landed in this "small dish" blog park, a lot of feelings. "Xiaocai" is my previous online name in QQ group. At the same time, I applied for this blog Park account. Five years ago, "Xiaocai" was very active in two groups of NET and C++ and was very enthusiastic to help netizens solve technical problems as far as possible. I vaguely remember the "Chrysanthemum, Allen, Cool" in the NET group at that time, and the "Teacher Xia, Brother Kite" and other netizens and brothers in the C++ group. Time has changed, but later for some reasons slowly faded out of the QQ group of technical exchanges, here I really thank the netizens "brother Yu" recommended me to work in a company in Beijing, but also miss the netizens who talked about everything.
There are a lot of off-topic topics. I hope to understand them and get directly into the topic. I have written three WEB version plug-in frameworks successively, which are based on WEBFORM platform, ASPNET MVC platform and ASPNET MVCCORE platform. Today, I share with you that I used to be responsible for a WEB plug-in framework based on ASP NET MVC platform, named "Antiquated", which is out of date because now NETCORE is popular.
Before entering the theme officially, I want you to see the effect first. Because it's a picture recording, I clicked and recorded it casually.
 
Plug-in framework
Plug-ins, as I personally understand them, can be viewed as separate plug-ins, ranging from large modules to small methods and even a partial display of a page. From the developer's point of view, it has the characteristics of clear structure, independence, low coupling, easy maintenance, and can realize hot plugging. Of course, the concept of plug-in as small as method or partial display is only recognized after contacting NOP, because before that, plug-in framework based on WEBFORM platform was only implemented as a module-based plug-in framework. The above is only my personal understanding. Do not spray if you don't like it.
Framework is a frame, which refers to its constraints, but also a shelf, which refers to its support. It is a basic conceptual structure for solving or dealing with complex problems, which is the definition of Baidu Encyclopedia. Generally speaking, the framework is an infrastructure, such as the construction industry, the design of residential areas, the foundation structure of houses and so on. Software systems in IT industry are similar. Framework bears the characteristics of security, stability, rationality and so on. A good basic framework should have the above characteristics. The purpose of this paper is to discuss with you the idea of implementing a framework, not to study a technical point in depth.
The rationality of the design is more important than the design itself. I have contacted many industries and seen some internal development frameworks, which are overstaffed for design. I have written the framework of communication class before. If you adopt OO design completely, you will lose a lot of performance problems. To return to the truth, Plug-in application framework can be understood as an application framework which carries a variety of forms of hot plug-in of independent plug-ins. You'd better have caching in application framework. We can understand it as one-level caching, logging, authentication, authorization, task management, file system and other basic functions, and prov IDE default implementations. For later customization, we should also be able to easily implement the adaptability of relevant functional points. The application framework is not so-called from scratch, we can choose the appropriate WEB platform to customize according to business needs and human resources. All of Microsoft's official WEB platforms E very extensible basic platforms, unified pipeline design, so that we can multi-dimensional entry and customization. As an application framework, it will certainly involve a large number of entities. At this time, we may encounter several problems, such as entity creation and life cycle management. If we use the original New operation, even if yo. U can play all the creative design patterns very well, it will be a headache. For the special framework ASPNET MVC in MVC architecture mode, the term "special" is used to modify it because ASPNET MVC should be implemented based on a variant MVC architecture, in which the model is View Model, so we need to map between the domain model and ViewM Odel. These are some personal experiences and opinions on the analysis of problems in work. If you are wrong, forgive me!
"Antiquated" plug-in framework refers to open source projects such as NOP and KIGG. According to the above ideas, the technologies used are MVC5+EF6+AUTOMAPPER+AUTOFAC+Autofac.Integration.Mvc+Enterprise Library.
Antiquated supports multiple themes, multilingualism, system settings, role permissions, logs, and so on.
Project catalog structure
The project directory structure is a classical "three-tier structure". These three tiers are not the other three tiers. Of course, I divide them into file directories. It is divided into Infrastructures, Plugins and UI. Look at the diagram.
Catalogue Explanation:
Infrastructures include three projects: Core, Database, Services and PublicLibrary. Their relationship is similar to that of "adaptation", and can also be understood as the adapter pattern in the design pattern. Core includes the basic supporting components of the whole project, the default implementation, and the "specification" of domain objects.
SQLDataBase is EF For SqlServer. Services serve domain objects. PublicLibrary is the default implementation of basic functions such as logging, caching, IOC, etc.
The Plugins folder contains all individual plug-ins, and Test1 is a page plug-in that is displayed in an area of the page. Test2 is a Fun plug-in that contains only one way to get data.
UI includes foreground display and background management
The Framwork folder is mainly an extension of the ASPNET MVC infrastructure. Having said so much vernacular, let's look at the implementation and effect of the code.
The whole application framework I focus on two parts of the basic functions and plug-ins. Let's first look at the entry Global.asax. Let's look at the instructions for the code. I'll just pick out some important codes for analysis and explanation. The relevant text comments are also made in more detail, and the code is relatively simple and clear. See the code.
Base part
protected void Application_Start()
        {
            // Engine Initialization
            EngineContext.Initialize(DataSettingsHelper.DatabaseIsInstalled());
            // Add custom model bindings
            ModelBinders.Binders.Add(typeof(BaseModel), new  AntiquatedModelBinder());
            if (DataSettingsHelper.DatabaseIsInstalled())
            {
                // empty mvc All viewengines
                ViewEngines.Engines.Clear();
                // Registration Customization mvc viewengines
                ViewEngines.Engines.Add(new ThemableRazorViewEngine());
            }
            // Custom metadata validation
            ModelMetadataProviders.Current = new AntiquatedMetadataProvider();
            
            AreaRegistration.RegisterAllAreas();
            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);
            DataAnnotationsModelValidatorProvider
                .AddImplicitRequiredAttributeForValueTypes = false;
            // Registration model validation
            ModelValidatorProviders.Providers.Add(
                new FluentValidationModelValidatorProvider(new  AntiquatedValidatorFactory()));
            // Register Virtual Resource Provider
            var viewResolver =  EngineContext.Current.Resolve<IAntiquatedViewResolver>();
            var viewProvider = new  ViewVirtualPathProvider(viewResolver.GetEmbeddedViews());
            HostingEnvironment.RegisterVirtualPathProvider(viewProvider);
        }
When we develop systems or application frameworks, we usually find the appropriate entry points provided by the basic framework to achieve global initialization. I believe that the friends who play ASP.NET should be familiar with the Global.asax file, or his base class HttpApplication, roughly speaking, this HttpApplication object, the creation and processing time of HttpApplication is after running HttpRuntime, and a little further forward is the IIS server container, so HttpApplication is the entry point we are looking for.
EngineContext first looked at the name of a scary, haha, in fact, it is still a relatively simple object, let's call it "core object context" for the time being, a little personal advice, when we do application framework, it is better to have such a core object to manage the life cycle of all basic objects. First up code
/// <summary>
/// Initialization engine Core object
/// </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;
}
Its responsibility is still relatively simple. It is responsible for creating and initializing the core object Engine in the form of thread security in the singleton mode. Of course, it has a second responsibility to encapsulate the core object of Engine and see the code.
public static IEngine Current
{
     get
     {
         if (Singleton<IEngine>.Instance == null)
         {
              Initialize(true);
         }
         return Singleton<IEngine>.Instance;
     }
}
Please pay attention to a small detail, the naming of EngineContext-Engine and the context of xxContext. When we read Microsoft open source code, such as ASPNET MVC WEBAPI, we often encounter this type of naming. Personal understanding,
Context is the division of logical business scope, object management and data sharing. Let's go on to see what's actually done in Engine, what objects are initialized, and what code is added.
    /// <summary>
    /// IEngine
    /// </summary>
    public interface IEngine
    {
        /// <summary>
        /// ioc container
        /// </summary>
        IDependencyResolver ContainerManager { get; }
        /// <summary>
        /// engine Initialization
        /// </summary>
        /// <param name="config">engine To configure</param>
        /// <param name="databaseIsInstalled">database initialized</param>
        void Initialize(AntiquatedConfig config, bool databaseIsInstalled);
        /// <summary>
        /// Reverse object-generic paradigm
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        T Resolve<T>() where T : class;
        /// <summary>
        /// Reverse object
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        object Resolve(Type type);
        IEnumerable<T> ResolveAll<T>();
    }
One is to initialize the IDependencyResolver container, which is not the built-in container in the MVC framework, but our custom container interface, as we will see later. Second, initialization of global configuration of basic objects.
Third, background task execution. Fourth, it provides the external interface of container inversion. Of course, I have a little contradiction in this place. Should I put it here, but IOC container itself should provide better external interface? I don't know. Let's do that for the time being. See here, we call this object engine core object should be more appropriate.
Let's focus on the IDependencyResolver container and task Task
 /// <summary>
    /// ioc Container interface
    /// </summary>
    public interface IDependencyResolver : IDisposable
    {
        /// <summary>
        /// Reverse object
        /// </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>
        /// Injected object
        /// </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>();
    }
There is not much to say about the function of container interface itself. It is standard operation. Those who have played with container should be familiar with it. Next we will focus on the creation and adaptation of containers. The creation of containers is entrusted to the IDependencyResolverFactory factory, and the IDependencyResolverFactory interface is defined as follows
public interface IDependencyResolverFactory
    {
        IDependencyResolver CreateInstance();
    }
The IDependencyResolverFactory creates containers for a method, and its implementation class DependencyResolverFactory implements specific object creation. Look at the code
public class DependencyResolverFactory : IDependencyResolverFactory
    {
        private readonly Type _resolverType;
        public DependencyResolverFactory(string resolverTypeName)
        {
            _resolverType = Type.GetType(resolverTypeName, true, true);
        }
        // Get from the configuration file ioc Container type
        public DependencyResolverFactory() : this(new  ConfigurationManagerWrapper().AppSettings["dependencyResolverTypeName"])
        {
        }
        // Reflection creates container objects
        public IDependencyResolver CreateInstance()
        {
            return Activator.CreateInstance(_resolverType) as IDependencyResolver;
        }
    }
<add key= "dependency Resolver Type Name" value= "Antiquated. PublicLibrary. AutoFac. AutoFacDependency Resolver, Antiquated. PublicLibrary"/> I posted the configuration nodes together, and the code logic is relatively simple. As you can see, the whole process of creating container objects is based on a standard factory mode, through reflection. Next, let's look at the specific ioc container DefaultFacDependency Resolver created and see the code.
public class DefaultFacDependencyResolver : DisposableResource,  
Core.Ioc.IDependencyResolver, // This is the container interface we pasted on it.
IDependencyResolverMvc // MVC Built-in container interface object, implementation mvc Global container injection
{
        // autofac container
        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 object
            _container = containerBuilder.Build();
        }
        // ...... Eliminate other code here
}
DefaultFacDependency Resolver, as its name implies, is the default container object of our application framework. That is to say, the application framework mentioned above should have a default implementation of basic functions, and can easily adapt to new functional components. For example, our default IOC container is Autofac, which is still available for the time being.
It's a good choice, lightweight, high performance and so on. If one day Autofac is not updated, or there are better or more suitable IOC containers, according to the open and close principle, we can easily adapt to the new IOC containers and reduce maintenance costs. The whole pipeline of the IOC container is almost finished. Let's look at the task below.
Definition of IBootstrapper Task.
/// <summary>
    /// Background tasks
    /// </summary>
    public interface IBootstrapperTask
    {
        /// <summary>
        /// Perform tasks
        /// </summary>
        void Execute();
        /// <summary>
        /// Task sequencing
        /// </summary>
        int Order { get; }
    }
The definition of IBootstrapperTask is simple, an Execute method and an Order sorting attribute. Next, let's look at the execution mechanism of background tasks in IEngine.
public class Engine : IEngine
    {
        public void Initialize(AntiquatedConfig config, bool databaseIsInstalled)
        {
                // Eliminate other members...
              ResolveAll<IBootstrapperTask>().ForEach(t => t.Execute());
        }
        // ...... Eliminate other code here
    }
The code is simple and clear. All task classes that have implemented the IBootstrapperTask interface are acquired by default container. Execute method is executed to implement the initialization of background tasks. So what functions can be implemented in the background task logic? Of course, there is no corresponding definition criterion for this, ah, my understanding is generally some public.
Basic functions, need to provide some basic data or initialization operations. For example, mail, default user data and so on. For example, one of our application frameworks has a background task, Automapper's mapping initialization operation, look at the code
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());
            // ...... Eliminate other code here
           
        }
    }
In this basic part, I have selected Engine, ioc, task and so on. Of course, Engine also includes some other contents, such as cache, log, global configuration, file system, authentication and authorization, etc. Because of the length of time, I will not introduce them one by one. Since it's a plug-in application framework, plug-ins are definitely necessary.
Now let's move on to the second part, plug-ins.
Plug-in part
The IPlugin plug-in interface is defined as follows
/// <summary>
    /// Plug-in unit
    /// </summary>
    public interface IPlugin
    {
        /// <summary>
        /// Plug-in Description Object
        /// </summary>
        PluginDescriptor PluginDescriptor { get; set; }
        /// <summary>
        /// Install plug-ins
        /// </summary>
        void Install();
        /// <summary>
        /// Uninstall plugins
        /// </summary>
        void Uninstall();
    }
 
The IPlugin plug-in interface consists of three members, one attribute plug-in describing objects, and two methods of installation and uninstallation. It's easy to understand how to install and uninstall, so let's look at the definition of Plugin Descriptor
    /// <summary>
    /// Plug-in Description Object
    /// </summary>
    public class PluginDescriptor : IComparable<PluginDescriptor>
    {
        public PluginDescriptor()
        {
        }
        /// <summary>
        /// Plug-in unit dll File name
        /// </summary>
        public virtual string PluginFileName { get; set; }
        /// <summary>
        /// type
        /// </summary>
        public virtual Type PluginType { get; set; }
        /// <summary>
        /// Plug-in Attribution Group
        /// </summary>
        public virtual string Group { get; set; }
        /// <summary>
        /// Alias, friendly names
        /// </summary>
        public virtual string FriendlyName { get; set; }
        /// <summary>
        /// Plug-in system name, an alias
        /// </summary>
        public virtual string SystemName { get; set; }
        /// <summary>
        /// Plugin version
        /// </summary>
        public virtual string Version { get; set; }
        /// <summary>
        /// Plugin author
        /// </summary>
        public virtual string Author { get; set; }
        /// <summary>
        /// Display order
        /// </summary>
        public virtual int DisplayOrder { get; set; }
        /// <summary>
        /// Is it installed?
        /// </summary>
        public virtual bool Installed { get; set; }
        // Eliminate other code...
    }
From the definition of Plugin Descriptor, we know that it is a description of plug-in information. For plug-in application frameworks, there will be a large number of plug-ins involved, so how do we manage these plug-ins? Let's move on to Plugin Manager, the plug-in management object.
 
// Self-execution at assembly loading
[assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")]
namespace Antiquated.Core.Plugins
{
    /// <summary>
    /// Plug in management
    /// </summary>
    public class PluginManager
    {
        // ...... Eliminate other code here
        private static readonly ReaderWriterLockSlim Locker = new  ReaderWriterLockSlim();
        private static readonly string _pluginsPath = "~/Plugins";
        
        /// <summary>
        /// Plug-in management initialization operation
        /// </summary>
        public static void Initialize()
        {
            using (new WriteLockDisposable(Locker))
            {
                
                try
                {
                    // ...... Eliminate other code here
                    // Load all plug-in description files
                    foreach (var describeFile in  pluginFolder.GetFiles("PluginDescribe.txt", SearchOption.AllDirectories))
                    {
                        try
                        {
                            // analysis PluginDescribe.txt File acquisition describe Description object
                            var describe =  ParsePlugindescribeFile(describeFile.FullName);
                            if (describe == null)
                                continue;
 
                            // Parsing plug-in installed
                           describe.Installed = installedPluginSystemNames
                                .ToList()
                                .Where(x => x.Equals(describe.SystemName,  StringComparison.InvariantCultureIgnoreCase))
                                .FirstOrDefault() != null;
                            
                            // Get all plug-ins dll file
                            var pluginFiles =  describeFile.Directory.GetFiles("*.dll", SearchOption.AllDirectories)
                                .Where(x => !binFiles.Select(q =>  q.FullName).Contains(x.FullName))
                                .Where(x => IsPackagePluginFolder(x.Directory))
                                .ToList();
                            //Parsing plug-in dll Main program set
                            var mainPluginFile = pluginFiles.Where(x =>  x.Name.Equals(describe.PluginFileName,  StringComparison.InvariantCultureIgnoreCase))
                  .FirstOrDefault(); describe.OriginalAssemblyFile
= mainPluginFile; // Add plug-in assembly references foreach (var plugin in pluginFiles.Where(x => !x.Name.Equals(mainPluginFile.Name, StringComparison.InvariantCultureIgnoreCase))) PluginFileDeploy(plugin); // ...... Eliminate other code here } catch (Exception ex) { thrownew Exception("Could not initialise plugin folder", ex);; } } } catch (Exception ex) { thrownew Exception("Could not initialise plugin folder", ex);; } } } /// <summary> /// Plug-in file replica deployment and addition to application domain /// </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 Move plug-in files to specified folders restrictedPlug = InitializePluginDirectory(plug, restrictedTempCopyPlugFolder); // Code is omitted here... var restrictedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(restrictedPlug.FullName)); BuildManager.AddReferencedAssembly(restrictedAssembly); return restrictedAssembly; } /// <summary> /// Plug-in installation /// </summary> /// <param name="systemName"></param> public static void Installed(string systemName) { // Eliminate other code here.... // Get all installed plug-ins var installedPluginSystemNames = InstalledPluginsFile(); // Get the installation status of the current plug-in bool markedInstalled = installedPluginSystemNames .ToList() .Where(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) .FirstOrDefault() != null; // If the current plug-in state is not installed, add it to the list to be installed if (!markedInstalled) installedPluginSystemNames.Add(systemName); var text = MergeInstalledPluginsFile(installedPluginSystemNames); // write file File.WriteAllText(filePath, text); } /// <summary> /// Plug-in uninstall /// </summary> /// <param name="systemName"></param> public static void Uninstalled(string systemName) { // Eliminate other code here.... // Logic is the same as above. File.WriteAllText(filePath, text); } }
From the part of code implementation of Plugin Manager, it mainly does several things: 1: loading all plug-in assemblies, 2: parsing all plug-in assemblies and initializing, 3: adding assembly references to application domain, 4: writing plug-in file information, and finally responsible for the installation and uninstallation of plug-ins. E The core code of plug-in management, and the code comments are more detailed. You can take a little time to look at the code and sort out the implementation logic. Please pay attention to the code marked red in the middle, which is also easy to achieve plug-in function problems. First we see this line of code [assembly: PreApplication Start Method]( Typeof (Plugin Manager), "Initialize"], which is a new extension point for ASP.NET 4.0 and above, has two functions: one is to dynamically add dependencies on external assemblies in conjunction with BuildManager.AddReferencedAssembly(), and the other is to enable our Initialize plug-in initialization function to execute in our Glo Before the_Start() method of Application.asax, because Microso. FT officially described the BuildManager.AddReferenced Assembly method must be executed before the Application_Start method. Finally, there is a small point to pay attention to. Some friends may want to copy the plug-in copy file to the Dynamic Directory directory of the application domain, that is, the compiled directory of ASP. NET. If you copy t. O This directory, you must pay attention to the issue of permission, CLR code access security (CAS). CAS code access security is a CLR level thing, interested friends can go to understand, it can help us solve many wonderful problems in future development.
Plug-in Business Logic Implementation
First of all, there are many ways for MVC to implement plug-in functions, and even the one I'm going to explain is rather troublesome. The reason why I chose this kind of explanation is to let us have a more comprehensive understanding of Microsoft's web platform and ASPNET MVC framework itself. Later, I will also explain a slightly simpler implementation. Let's move on. Let's move to Global.asax for the moment and see the code.
     
   /// <summary>
        /// System initialization
        /// </summary>
        protected void Application_Start()
        {
            // Eliminate other code here...
            // Register Virtual Resource Provider
            var viewResolver =  EngineContext.Current.Resolve<IAntiquatedViewResolver>();
            var viewProvider = new  ViewVirtualPathProvider(viewResolver.GetEmbeddedViews());
            //register
            HostingEnvironment.RegisterVirtualPathProvider(viewProvider);
        }
Get an IAntiquatedViewResolver object from the EngineContext context object. What exactly is the IAntiquatedViewResolver object? How to define it? Let's keep looking down.
public interface IAntiquatedViewResolver
    {
        EmbeddedViewList GetEmbeddedViews();
    }
IAntiquated ViewResolver defines a method that literally means to get all the embedded views resources, and yes, it does that. Do you think the implementation of plug-ins is a bit eyebrow? Ha-ha. Don't worry, let's move on to the second object, the ViewVirtualPathProvider object.
   
 /// <summary>
    /// Virtual Resource Provider
    /// </summary>
    public class ViewVirtualPathProvider : VirtualPathProvider
    {
        /// <summary>
        /// Embedded view resource list
        /// </summary>
        private readonly EmbeddedViewList _embeddedViews;
        /// <summary>
        /// Object initialization
        /// </summary>
        /// <param name="embeddedViews"></param>
        public ViewVirtualPathProvider(EmbeddedViewList embeddedViews)
        {
            if (embeddedViews == null)
                throw new ArgumentNullException("embeddedViews");
            this._embeddedViews = embeddedViews;
        }
        
        /// <summary>
        /// Rewrite base class FileExists
        /// </summary>
        /// <param name="virtualPath"></param>
        /// <returns></returns>
        public override bool FileExists(string virtualPath)
        {
            // If the virtual path file exists
            return (IsEmbeddedView(virtualPath) ||
            Previous.FileExists(virtualPath));
        }
        /// <summary>
        /// Rewrite base class GetFile
        /// </summary>
        /// <param name="virtualPath"></param>
        /// <returns></returns>
        public override VirtualFile GetFile(string virtualPath)
        {
            // Determine whether it is a virtual view resource
            if (IsEmbeddedView(virtualPath))
            {
                // Partial Code Ellipsis...
                // Getting virtual resources
                return new EmbeddedResourceVirtualFile(embeddedViewMetadata,  virtualPath);
            }
            return Previous.GetFile(virtualPath);
        }
    }
The core of the members defined in the ViewVirtualPathProvider is a list and two methods, which are not defined by itself but are methods in the overridden VirtualPathProvider base class. I think the definition and logic of ViewVirtualPathProvider itself are very simple, but in order to better understand such a virtual resource object, we need to understand its base class, the virtual resource provider VirtualPathProvider.
Virtual PathProvider Virtual Resource Provider, described on MSDN, provides a set of methods to enable Web applications to retrieve resources from virtual file systems, which belong to System. Web. System. Web is a universal assembly except ASP.NETCORE, which can be seen in all Microsoft's WEB development platforms before. Let's talk about System.Web for a while. Let's move on to VirtualPathProvider objects.
public abstract class VirtualPathProvider : MarshalByRefObject
    {
        // Eliminate other code...
        protected internal VirtualPathProvider Previous { get; }
        public virtual bool FileExists(string virtualPath);
        public virtual VirtualFile GetFile(string virtualPath);
 }
From the definition of VirtualPathProvider object, it is related to file resources. The request resources of WEBFORM platform correspond to the physical files under the server root directory, and if not, NotFound. What if we want to get resources from databases or embedded resources that depend on assemblies? It doesn't matter if Virtual Path Provider can help me solve it. The VirtualPathProvider derivative class ViewVirtualPathProvider registers through the Global.asax ostingEnvironment.Register VirtualPathProvider (viewProvider). All request resources must pass through it, so the processing of View view resources embedded in our plug-in assembly requires only two logical FileExists and GetFile. Let's take another look at the two logic of the ViewVirtualPathProvider implementation class. If it's an embedded resource, we implement our own GetFile logic to read the plug-in View file stream. Otherwise, it will be handled by default.
At this point, some friends may still be confused about the implementation mechanism of FileExists and GetFile. Okay, let me give you a brief introduction. It's my personality, ha-ha. To clarify this issue, we need to relate to our custom implementation of the Antiquated VirtualPath Provider View Engine, which inherits from the VirtualPath Provider View Engine. Let's first look at the definition of the VirtualPath Provider View Engine.
public abstract class VirtualPathProviderViewEngine : IViewEngine
    {
        // Eliminate other code...
        private Func<VirtualPathProvider> _vppFunc = () =>  HostingEnvironment.VirtualPathProvider;
        public virtual ViewEngineResult FindView(ControllerContext controllerContext,  string viewName, string masterName, bool useCache)
        {
             // Eliminate other code...
             GetPath(controllerContext, ViewLocationFormats, AreaViewLocationFormats,  "ViewLocationFormats", viewName, controllerName, 
              CacheKeyPrefixView, useCache, out viewLocationsSearched); } private string GetPath(ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName,
                    string name, string controllerName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations) { // Eliminate other code... // This method is called indirectly FileExists Method } protected virtual bool FileExists(ControllerContext controllerContext, string virtualPath) { return VirtualPathProvider.FileExists(virtualPath); } protected VirtualPathProvider VirtualPathProvider { get { return _vppFunc(); } set { if (value == null) { throw Error.ArgumentNull("value"); } _vppFunc = () => value; } } }
From the code implementation of the VirtualPathProviderViewEngine, we can see that the FindView method indirectly calls the FileExists method, while the FileExists method implements the logic through the FileExists method of the VirtualPathProvider object. Interestingly, the VirtualPathProvider attribute comes from the static property HostingEnvironment under the system. web. Hosting namespace. Ider, do you remember that we registered a derived class of VirtualPathProvider object in Global.asax? Let me stick out the code, HostingEnvironment.RegisterVirtualPathProvider(viewProvider), which is interesting. Is that the FileExists method logic of our Antiquated VirtualPathProvider View Engine that is the implementation in our VirtualPathProvider derived class? Yes, that's it. I say you may not believe it. Let's stick to the code. Now let's look at the partial implementation of the HostingEnvironment object under System. web. hosting.
public static void RegisterVirtualPathProvider(VirtualPathProvider  virtualPathProvider)
    {
      // Eliminate other code...
      HostingEnvironment.RegisterVirtualPathProviderInternal(virtualPathProvider);
    }
internal static void RegisterVirtualPathProviderInternal(VirtualPathProvider  virtualPathProvider)
    {
      // Our derived classes are assigned to_theHostingEnvironment it
      HostingEnvironment._theHostingEnvironment._virtualPathProvider =  virtualPathProvider;
      virtualPathProvider.Initialize(virtualPathProvider1);
    }
Our Global.asax calls the RegisterVirtualPathProvider method, which internally calls a protected method, RegisterVirtualPathProvider Internal, which assigns our VirtualPathProvider derived class to the _the HostingEnvironment field. Now, if we only need to find the packaging properties of this field, whether it is the source of the problem will be solved. Look at code
public static VirtualPathProvider VirtualPathProvider
    {
      get
      {
        if (HostingEnvironment._theHostingEnvironment == null)
          return (VirtualPathProvider) null;
        // Ellipsis code...
        return HostingEnvironment._theHostingEnvironment._virtualPathProvider;
      }
    }
Seeing the wrapping property of the _the HostingEnvironment field, does it make you feel very relaxed? Yes, the FileExtis implementation logic in our Antiquated VirtualPathProvider View Engine is the logic implemented in our own defined ViewVirtualPathProvider. So far, the implementation mechanism of FileExists has been fully introduced. Next, we will continue to analyze the implementation mechanism of GetFile, our second problem. I don't know if careful friends have found out that the implementation class Antiquated VirtualPathProviderViewEngine of our application framework mentioned above is inherited from VirtualPathProviderViewEngine to view the MVC source code. This object does not implement the GetFile method. So when and where was it called? In fact, if we are familiar with the internal implementation of MVC framework, we can easily locate where we are looking. We know that the rendering of View is done by IView, and ASPNET MVC cannot compile View files. Based on these two points, let's first look at the definition of IView.
public interface IView
    {
        // view Present
        void Render(ViewContext viewContext, TextWriter writer);
    }
The definition of IView is very clean. There is only one member responsible for rendering the View. For the sake of intuition, let's look at the definition of the only direct implementation class of IView, BuildManagerCompiledView, and see the code.
public abstract class BuildManagerCompiledView : IView
    {
        // Other members...
        public virtual void Render(ViewContext viewContext, TextWriter writer)
        {
            
            // Compile view file
            Type type = BuildManager.GetCompiledType(ViewPath);
            if (type != null)
            {
                // activation
                instance = ViewPageActivator.Create(_controllerContext, type);
            }
            
            RenderView(viewContext, writer, instance);
        }
        protected abstract void RenderView(ViewContext viewContext, TextWriter  writer, object instance);
    }
As can be seen from the definition of BuildManagerCompiledView, the Ender method of IView does three things. 1. Get the compiled WebViewPage type of View file, 2. Activate WebViewPage, 3. Presentation. The call to GetFile is in BuildManager.GetCompiledType(ViewPath); in this method, the assembly to which BuildManager belongs is System.web. We went on to look at the System.web source code and finally found that the call to GetFile was the GetFile method, which is the override method of the ViewVirtualPathProvider object registered in Global.asax. Look at the code. Because there are too many call stacks, I paste the last part of the code.
public abstract class VirtualPathProvider : MarshalByRefObject
  {
    private VirtualPathProvider _previous;
    // Other members...
    public virtual VirtualFile GetFile(string virtualPath)
    {
      if (this._previous == null)
        return (VirtualFile) null;
      return this._previous.GetFile(virtualPath);
    }
}
Now you have a thorough understanding of the VirtualPathProvider object delivery mechanism and its important role in our plug-in application framework? Okay, that's the end of the problem. Let's continue with the plug-in implementation above.
Next, let's continue to look at the installation and uninstallation of plug-ins.
install
See the code logic of the Installed method in Plugin Manager above. It should be noted that in order to achieve hot plug-in effect, the HttpRuntime.UnloadAppDomain() method needs to be called after installation and uninstallation to restart the application domain and reload all plug-ins. So far, the whole plug-in implementation principle has ended. Heart plug, but we haven't finished yet. Next, let's look at the directory structure of the individual plug-ins.
Plug-in instance
 
These are two Demo plug-ins, which are meaningless. The Test1 plug-in is a plug-in for the display class, which can be displayed in every corner you want to display. Test2 plug-in is a data plug-in, mainly for data acquisition. Demo only lists two smaller plug-in assemblies, you can also achieve larger, such as the plug-in of the entire module function and so on. Looking at the directory structure of Test1 plug-in, I wonder if careful friends have found a serious problem? Friends familiar with ASPNET MVC view compilation principle should all know that view compilation needs web.config file participation. View compilation operation occurs in the Compile method of AssemblyBuilder object in System.Web assembly. Obtaining web.config node is in the BuildProviders Compiler object. Because the system.web seems to have no open source and the code logic is messy, I will not paste the code. Interesting friends can decompile to see. Let's go back to the directory structure of the Test1 plug-in. Why can you compile the View file without a web. config file under the Views of the standard ASPNET MVC site? In fact, the greatest contribution is the FileExists and GetFile methods implemented by the implementation class View VirtualPathProvider, which we explained in detail above. Of course, another contributor is also the implementation class Antiquated VirtualPath Provider View Engine mentioned above. I will not post the specific code, but I will talk about the implementation principle. For the ASPNET MVC infrastructure framework, it only needs to know whether there is a view file corresponding to this virtual path and get the view file, and finally compile the view. We can use this feature, if it is a plug-in view, the view virtual path matched by FindView or FindPartial View implemented by ourselves in View Engin (even the path returned by the plug-in view is also the virtual path under the root directory Views) combined with FileExists and GetFile to realize the generation, compilation and final rendering of the plug-in view. If it is a non-plug-in view, it is directly handed over to the ASPNET MVC infrastructure for execution. A base class that configures View compilation is required under the root directory views.
Now let's see how to implement the new plug-in. Your plug-in can be a library assembly or a complete ASP. NET MVC website. Take Test1 as an example. 1. First, we need to create a new PluginDescribe.txt text file. The main purpose of this file is to initialize the PluginDescriptor members of the IPlugin implementation class. Let's look at the contents in detail.
FriendlyName: Test Test1Plugin Display
SystemName: Test.Test1Plugin.Display
Version: 1.00
Order: 1
Group: Display
FileName: Test.Test1Plugin.Display.dll
2. Create a new class xxx, name selection, need to implement IPlugin interface, also with the Test1 plug-in as the column
public class Test1Plugin : BasePlugin, IDisplayWindowPlugin
    {
        public string Name { get { return "Test.Test1Plugin.Display"; } }
        public void GetDisplayPluginRoute(string name, out string actionName, out  string controllerName, out RouteValueDictionary routeValues)
        {
            actionName = "Index";
            controllerName = "Test1";
            routeValues = new RouteValueDictionary
            {
                {"Namespaces",  "Antiquated.Plugin.Test.Test1Plugin.Display.Controllers"},
                {"area", null},
                {"name", name}
            };
        }
    }
 
public interface IDisplayWindowPlugin: IPlugin
    {
         string Name { get; }
        void GetDisplayPluginRoute(string name, out string actionName, out string  controllerName, out RouteValueDictionary routeValues);
    }
3. If there is a view view, it must be an embedded resource for reasons I have already explained clearly.
4. If there is a need to implement routing registration, let's look at the RouteProvider implementation of Test1.
Public class RouteProvider : IRouteProvider
    {
        public void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute("Plugin.Test.Test1Plugin.Display.Index",
                 "Plugins/Test1/Index",
                 new { controller = "Test1", action = "Index" },
                 new[] { "Test.Test1Plugin.Display.Controllers" }
            );
        }
        public int Priority
        {
            get
            {
                return 0;
            }
        }
    }
5. It's also the last step. You need to generate the assembly under the Plugins file in the website root directory. These are the principles and logic of the whole plug-in implementation. To put it aside, ASP. NET MVC implements plug-ins in many ways, even in simpler ways. The reason I chose this way is that I think this way of implementation can understand the whole context more. Now let's take a look at another implementation.
ASP.NET MVC Plug-in Implementation II
1. Plug-in management is based on the above. Plgin Management includes loading, initialization, installation, uninstallation and other functions.
2. We do not need to register and rewrite the VirtualPathProvider object in Global.asax.
3. Independent plug-in projects do not need to be added to embedded resources if they have View View files, but they need to be copied to the root directory Plugins according to the directory structure. In addition, web.config files must be added and copied to the root directory Plugins under the directory. webconfig needs to specify the conditions required for View compilation.
4. View in Action returns, specifying the relative path of the website root directory directly.
Do you think the second way is very simple? That's all for plug-ins. It was intended to introduce several basic modules, such as multi-language, authentication and authorization, multi-topic, etc. Because of the space problem, I will conclude with a brief discussion on the implementation of multi-themes.
Multi-topic Implementation
For web front-end, multi-theme is actually the category of CSS style implementation. Our application framework implements multi-theme by switching CSS style implementation according to different theme patterns. Look at the picture
The contents of the Themes folder located in the website root directory are the directory structure of the website theme. Each topic has its own separate style files styles.css and Head.cshtml view files. The content of the view file is very simple, that is to return the style.css file path. Combined with Themes directory structure, I focus on introducing the principle of multi-topic implementation. Subject switching is actually the main style file switching of css, so how can we achieve the main style file switching of view? Simply rewrite the FindPartialView and FindView method logic of ViewEngin to switch and import CSS files through views.
1. Customize view Path Template of view Engin Engine.
2.ViewEngin's FindPartialView logic.
Ouch, it's finally finished. It's not easy to send a letter. Please give us more praise. Thank you.
Finally, I want to say a few more words. NET is in a bad market now, at least in Changsha. This is mainly because of the poor ecosystem of Net. Recently, it seems that Changsha NET community is going to hold a technology conference to promote NetCore. Here I wish the developer technology conference a great success. At the same time, I hope you will make more contributions to Netcore ecology. Next time I will continue to share an application framework I implemented in my previous work, which is based on ASP.NETCORE.
Finally, thank you for your support. Source code will be uploaded to github in the follow-up meeting, because it is still being sorted out.
Code word is not easy, if it helps you, also trouble to point rmb support, thank you!

 

Posted by anybody99 on Wed, 08 May 2019 09:42:39 -0700