Orchard module loader and dynamic compilation
As a composable CMS, Orchard has the ability to load an arbitrary set of modules (also known as "extensions") at run-time. One of the goals of the 0.5 release was to make the process of installing and updating modules as easy as possible.
Orchard, as any ASP.NET MVC application, supports loading module compiled as assemblies using Visual Studio. Orchard also offers a customized module loading strategy which, for example, allows loading assemblies for modules without having to deploy them in the "~/bin" folder.
In addition to that, Orchard supports (this is still somewhat experimental) the ability to dynamically compile modules deployed as source code only. This is more flexible than deploying binaries, and enables some interesting scenarios such as "in place" code customization without having to use Visual Studio. This is somewhat similar to the ASP.NET "App_Code" directory, except Orchard supports multiple "logical folder" (typically one per module) independently.
The goal of this section is to describe at a technical level how Orchard load modules in the 0.5 release. This feature is often referred to as "Orchard Dynamic Compilation", even though technically dynamic compilation is only involved in very specific cases.
High Level Overview
When an Orchard application starts, the Orchard Framework (the ExtensionLoaderCoordinator class to be precise) needs to figure out what are the modules installed in the Web Site and activate them (typically by loading their assembly).
At a high level, this process can be divided in 3 distinct phases:
- Discovery: figure out what are the modules present in the web site
- Activation: figure out what strategy to use to "activate" (or load) each module
- References Resolution: figure out what are the assembly references needed to be activated for each module. This phase is technically part of the "Activation" phase, but it is easier to think about the problem of reference resolution as a separate concern.
Once modules are properly activated, they are further examined to detect and enable individual features, but this is a topic for another section.
The list of available extensions in an Orchard installation is built by searching various folders of the file system for "module.txt" and "theme.txt" files. The folders looked at by default are listed in the following sections.
The "~/Modules" folder is intended to contain the vast majority of Orchard modules. The convention is that each module is stored in a sub-folder named "<ModuleName>" containing a single "module.txt" file. Packaging, distribution and sharing of modules is only supported for modules in the "~/Modules" folder.
The "~/Core" folder contains, by convention, modules defined in the "Orchard.Core" assembly. These modules are part of the "Core" Orchard system and are not intended to be modified as freely as modules in the "~/Modules" folder.
The "~/Themes" folder is intended to contain Orchard Themes. Wrt to dynamic compilation, Themes are treated almost exactly the same as Modules, except that Themes don't have to have code (assembly in bin or .csproj file). For the rest of this page, when we refer to "Module", it should be understand that the concept applies to "Theme" the same way.
Orchard 1.10 introduced a new feature that allows the loading of extensions from custom-defined folders outside of the ones listed above by adding the
ExtensionLocations service that is utilised by each extension loader (see the Loaders in the Activation section below).
Additional extension folders can be configured by defining an AppSetting (e.g. by adding it to the root web.config file, which contains appropriate examples) with the
key "Modules" and/or "Themes" with their respective
value being e.g. "~/Modules.Custom" and/or "~/Themes.Custom".
Here is an example of an Orchard installation which contains the following extensions: Common and Localization (Core modules), Orchard.Azure and Orchard.Caching (built-in modules), SafeMode and TheAdmin (built-in themes), MyModule1 and MyModule2 (custom modules), MyBaseTheme and MyTheme (custom themes).
Root (Orchard.Web) Core Common module.txt <= "Common" module from "Core" Localization module.txt <= "Localization" module from "Core" Modules Orchard.Azure module.txt <= "Orchard.Azure" module Orchard.Caching module.txt <= "Orchard.Caching" module Modules.Custom MyModule1 module.txt <= "MyModule1" module MyModule2 module.txt <= "MyModule2" module Themes SafeMode theme.txt <= "SafeMode" theme TheAdmin theme.txt <= "TheAdmin" theme Themes.Custom MyBaseTheme theme.txt <= "MyBaseTheme" theme MyTheme theme.txt <= "MyTheme" theme
Once Orchard has collected all the "Module.txt" files from the discovery phase, Orchard uses distinct strategies (or "Module Loaders") to load these modules in memory. Internally, the act of "loading a module" is an activity that takes a "module.txt" file as input and returns a list of "System.Type" as output. Note that this is slightly more generic than simply returning a "System.Assembly", as it allows Orchard to support multiple modules per assembly. For example, the "Orchard.Core.dll" assembly currently contains about 10 modules.
The Orchard framework currently implements the following loaders:
"Referenced Module" Loader
This loader looks in in "~/bin" directory for a assembly name corresponding to the module name specified in "module.txt". If the assembly exists, it is loaded and all its types are returned. This loader is useful when someone wants to deploy an Orchard web site where all modules are pre-compiled and stored in "~/bin", in a typical "asp.net web application" way.
"Core Module" Loader
If "module.txt" indicates a module from the "~/Core" folder, the CoreExtensionLoader returns the types from the "Orchard.Core.<moduleMame>" namespace of the "Orchard.Core" assembly. "Orchard.Core" is a special assembly containing modules that are "core" to the system, i.e. offering basic functionality on top of the Orchard Framework.
"Precompiled Module" Loader
If "module.txt" indicates a module from the "~/Modules" folder, the PrecompiledExtensionLoader looks for an assembly named "<ModuleName>" in the "~/Modules/<ModuleName>/bin" folder. If the file exists, it's is copied to the
~/App_Data/Dependencies folder, which is a special folder used by ASP.NET to look for additional assemblies outside of the traditional "~/bin" folder.
"Dynamic Module" Loader
If "module.txt" indicates a module from the "~/Modules" folder, the "Dynamic Module" loader looks for a file named "
Note: This loader is the only one in the system performing what is often referred to as "dynamic compilation", and is indeed optional if modules have been pre-compiled.
Since there is potentially more than one loader able to load a given module, Orchard has to have a way to resolve the ambiguity, i.e. pick the "right" loader. Each loader has the ability to return a "date of last modification" for each module they can load. For a given module, if there are multiple candidate loaders, Orchard will pick the loader which returns the most "recent" date of last modification.
For example, a given module can be distributed with both full source code (including .csproj file) and compiled into an assembly in its "bin" directory. The first time the module is loaded, Orchard will pick the loader for the assembly in "bin" since it's very likely the assembly was compiled after the last source code change was made. However, if any change was made to the source code afterward, the "Dynamic Module" loader will return the date of the most recently modified file (either the source file or csproj), and Orchard will pick that loader for the given module.
Note that the "Core Module" loader is never ambiguous, because there is only one way to load these modules. The ambiguity can only arise for modules in the "~/Modules" directory.
RootFolder Bin Orchard.Web.dll Orchard.Core.dll Foo.dll Core Common <= "Core Module" loader module.txt Localization <= "Core Module" loader module.txt Modules Foo <= "Reference Module" loader (because a "~/bin/Foo.dll" file exists) module.txt Bar <= "Precompiled Module" loader (because a "~/Modules/Bar/bin/Bar.dll" file exists) bin Bar.dll module.txt Baz <= "Dynamic Module" loader (because a "~/Modules/Baz/Baz.csproj" file exists) Controller BazControler.cs Baz.csproj module.txt
Disabling the "Dynamic Module" loader
The dynamic module loader should be useless when deploying a website in production, as a production enviroment it
should not be able to install and load module dynamically. But another important reason why it should be disabled
is that it creates a lot of
FileSystemWatcher instances to detect changes on the modules.
To disable the module, rename the file
then check the content is:
<?xml version="1.0" encoding="utf-8" ?> <HostComponents> <Components> <Component Type="Orchard.Environment.Extensions.ExtensionMonitoringCoordinator"> <Properties> <Property Name="Disabled" Value="true"/> </Properties> </Component> </Components> </HostComponents>
Deploy this file and restart the App Pool.
NB: You will have to ensure that the binaries for every modules are available in the
/bin folder of each module,
such that the Precompiled Module loader can use them directly. When using Visual Studio this should be the case.
Otherwise use the command line tool to build the website, which will have the same effect.
(TODO: Explain how Orchard figures out references by looking at the "References" section of the csproj file as well as looking at additional assembly binaries dropped in each module "bin" directory)
Change of Configuration Detection
As explained above, modules are loaded at application startup. However, once the application is started up, changes can happen: a new module might be installed, the source code of a module might be manually updated, a module might be removed from the site, etc. To detect these changes, Orchard asks each module loader in the system to "monitor" potential changes, and notify when a change happens.
When a change is detected, the current module configuration is discarded and modules are re-examined, loaded and activated as if the application was starting up again. In some cases, these changes require an ASP.NET AppDomain restart (e.g. a new version of a module assembly needs to be loaded). Orchard detects these situations and forces an ASP.NET AppDomain restart.
Rendering Web Forms Views
(TODO: Explain that Orchard uses a custom virtual path provider to insert custom "Assembly Src=xx" and "Assembly Name=xxx" directive when reading .ascx and .aspx files)
Rendering Razor Views
(TODO: Explain that Orchard uses a Razor custom API to add Module dependencies to Views)
This file contains the list of modules, their loader and their resolved references of the "last known good" configuration of module, i.e. the last time Orchard successfully loaded all modules of the application. Examining the content of this file can be useful for debugging purposes, i.e. if a the latest version of a module doesn't seem to be loaded, for example.