📜 ⬆️ ⬇️

Writing a modular application on .Net Framework

Current trends in the development of information systems require designers to lay in the system architecture the possibility of dynamically expanding their functionality. And despite the existence of a huge number of developments in this direction, there is no single solution to the structure of the modular application. The use of a ready-made solution is not always possible due to the specifics of a programming language or the system being developed. Also, ready-made solutions of modular systems are not always available for study, and sometimes unnecessarily complex.

Modules in different systems often have different limits of functionality. The system can be allocated some strictly defined points of expansion - some functionality, complemented by third-party developers. Or the system can only be a module management mechanism, and all its functionality is implemented by separate modules.


Stages of designing modular applications


To develop a modular application, first of all, it is necessary to select the functionality that should be expanded with the help of modules.
')
Next, interfaces are developed with which the system will turn to third-party implementations for this functionality.

The most sensitive point is the question of how to dynamically add an interface implementation.

Reflection


The .Net Framework features powerful reflection technology. Reflection allows the program to track and modify its own structure and behavior at run time.

In relation to our task, reflection allows you to load libraries into memory and check all the classes implemented in it for the implementation of the necessary interfaces.

That is, you can add libraries (with the implementation of dedicated interfaces), which are modules, to a special directory and using the technology of reflection to find and instantiate the necessary classes. This is the most popular solution that is often found on the Internet. But this approach has significant drawbacks associated with high resource intensity.

Loading an assembly into memory and enumerating all the classes available in it, in search of interface implementations, requires a large amount of RAM, and tangible time to perform the iteration. Everything is complicated if the implementation of the method is beyond the scope of one library and has dependencies on third-party libraries. Then assemblies that do not contain implementations of the necessary interfaces and the processor time spent on their research are wasted are brute force.

Module structure


Obviously, to exclude unnecessary libraries from brute force, additional information about the module is needed. A similar source of information can be a text file accompanying the module’s libraries and providing information about them. Thus, we are faced with the task of developing requirements for the structure of the module, one of the points of which can offer the requirement of having a file pointing to the main library with the implementation of interfaces and containing a list of all the necessary dependencies.

The simplest solution for a module device is the following:

1. The module is an archive of all necessary libraries. The compression algorithm can be zip. And, as such, compression is not required (binary libraries are difficult to archive), you just need to combine all the components of the module into one file.

2. In addition to libraries, the module must contain a text file (called a descriptor) containing information about the main library, dependencies, and, to improve performance by eliminating brute force, the name of the interface being implemented along with the full name of the class implementing it.

To implement the plugin handle, it seems convenient to use XML.

The simplest structure of such a document may be as follows:
<?xml version="1.0"?> <plugin> <type> </type> <name> </name> <description> .</description> <version>  </version> <class>   ,    </class> <assembly> </assembly> <dependences> <dependence> </dependence> </dependences> </plugin> 


Add and remove modules


Adding a new module to the system can occur in the following sequence:

1. The system is transferred the full path of the file with the added module.
2. The added module is checked for compliance with its descriptor: it checks the presence of all the specified libraries, the presence of the main class and the implementation of the specified interface.
3. A new subdirectory for the added module is created in the system directory allocated for storing the modules. All libraries of the module are copied to this directory.
4. A unique identifier * of the module is calculated (as an option, you can take a hash on behalf of the module and its version).
5. All information from the module descriptor and the calculated identifier are recorded in the system registry of modules (xml file that stores information about modules installed in the system).
________________________________________
* this identifier is entered in case it becomes necessary to save information about the use of the module in the last session of work in the system

In the simplest case, the register of modules may be an xml-file with a structure similar to the above, with the only difference being that there will be a lot of records about the modules in it:
 <?xml version="1.0"?> <plugins> <plugin> <id>534523</id> <type> </type> <name>  1</name> <description>  1</description> <version>   1</version> <class>   ,    </class> <assembly> </assembly> <dependences> <dependence> </dependence> </dependences> </plugin> <plugin>> <id>79568</id> <type> </type> <name>  2</name> <description>  2</description> <version>   2</version> <class>   ,    </class> <assembly> </assembly> <dependences> <dependence> </dependence> </dependences> </plugin> . . . . </plugins> 


The sequence of actions to remove the module:
1. Delete the module directory **.
2. Removing information about the module from the registry.

________________________________________
** here it is necessary to note the following nuance: the initiation of the removal of a module by the user can be performed while the system is actively using this module, which may result in blocking the libraries of the module. I’ll come back to this problem below.

Class structure


class diagram

PluginDescriptor - provides information about the module, sufficient to instantiate it.

PluginRegister - performs read and write operations to the modules registry. Returns the handle of the module corresponding to the specified identifier. May return a complete list of descriptors of all available modules.

Plugin - provides information about the module, based on the corresponding information in the descriptor. It contains a link to the object that represents the implementation of the module. Perhaps the existence of this class may seem redundant. But its value manifests itself in the event that the user makes a decision on the expediency of using the module on the basis of his (module) description. In this case, we can provide all the necessary information to the user without loading the module libraries.

PluginManager - performs the addition and removal of modules in the system. Monitors the integrity of the module when it is added. It is implemented in accordance with the template "loner", to unify ways to obtain modules.

PluginLoader - instantiates a module. The need to introduce this class is caused by the specifics of the process of instantiation itself, which will be discussed below (you can also read this article).

Nuances of dynamic connection of modules specific to .Net Framework


Dynamic loading of assemblies in .Net has a feature - assemblies are loaded into the so-called AppDomain - application domains, which are an isolated environment in which applications run. The nuance is that unloading the assembly separately is impossible. It can be made only by unloading the entire domain. Thus, after the module is initialized, all the libraries it uses are blocked for deletion from the file system, and their unloading from memory becomes impossible.

To resolve the issue of blocking library files, there is a solution called shadow copying. Its essence lies in copying the library file and loading it into memory already, its copy, while the original remains available for deletion.

The solution to the problem of unloading a previously loaded module from memory is to create a separate domain for the module. In addition to solving the problem of unloading a module from memory, this solution opens up the possibility of limiting the rights of executable code. For example, a module can be disabled from reading and writing in any directory other than the one in which it is located. But this solution has a downside, requiring serialization of classes that implement the interfaces of the modules.

For clarity, here’s the PluginLoader class code and a portion of the Plugin class code (the code does not take into account the dependencies of the loadable module):
 public class BasePlugin { ... /// <summary> ///    . /// </summary> /// <remarks> ///      . /// </remarks> public Object Instance { get { if (instance == null) { instance = LoadInstance(); } return instance; } } /// <summary> ///       ///     . /// </summary> /// <returns> ///     ,  null. /// </returns> private Object LoadInstance() { Descriptor d = this.Descriptor; /*   */ AppDomainSetup setup = new AppDomainSetup(); setup.ShadowCopyFiles = "true"; //    //TODO:      /*     */ AppDomain domain = AppDomain.CreateDomain(d.ToString(), null, setup); /*    */ PluginLoader loader = (PluginLoader)domain.CreateInstanceFromAndUnwrap( typeof(PluginLoader).Assembly.Location, typeof(PluginLoader).FullName); /*    */ Object obj = null; try { obj = loader.CreateInstance(d, PluginManager .GetPluginDirectory(this)); } catch (Exception e) { AppDomain.Unload(domain); throw new PluginLoadException(d, "", e); } return obj; } } 


 [Serializable] public class PluginLoader { /// <summary> ///       . /// </summary> /// <param name="d">  .</param> /// <param name="pluginDirectory"> ///      . /// </param> /// <returns> ///  ,   . /// </returns> public Object CreateInstance(Descriptor d, String pluginDirectory) { String assemblyFile = Path.Combine(pluginDirectory, d.AssemblyName); /*   . */ Assembly assembly = null; try { AssemblyName asname = AssemblyName.GetAssemblyName(assemblyFile); assembly = AppDomain.CurrentDomain.Load(asname); } catch (Exception e) { throw new AssemblyLoadException(assemblyFile, "  .", e); } /*    ,  . */ Object obj = null; try { obj = assembly.CreateInstance(d.ClassName); } catch (Exception e) { throw new ClassLoadException(d.ClassName, assembly.FullName, "    .", e); } return obj; } } 


Conclusion


In order not to throw an article on the bare code, I will summarize the above. All the above considerations were tested by me in just one project, so it is possible that your experience of following this article will cause difficulties that I have not noticed. Therefore, I will be glad to any comments and comments.

From the obviously unsolved problems, I can cite the following: excessive expenditure of resources is possible if the modules have the same libraries as dependencies. If there are interesting considerations on this issue - share.

Source: https://habr.com/ru/post/133235/


All Articles