Initialization
Product version: |
EPiServer CMS 6.0 |
---|---|
Document version: |
1.0 |
Document last saved: |
Introduction
The EPiServer CMS initialization system is designed and implemented with the purpose of being the sole initialization mechanism to use for both EPiServer internal code as well as third-party and custom modules. EPiServer CMS initialization system comprises:
- A discovery mechanism to determine which modules should be part of the initialization process
- A dependency sorting algorithm that decides the order of execution
- An execution engine that will execute the modules
- Handling of re-execution of initialization modules (by hooking into ASP.NET) in the occurence of exceptions during startup
- The namespace EPiServer.Framework.Initialization which resides in the assembly EPiServer.Framework
Table of Contents
- MEF - The Detection Mechanism
- Dependency sorting
- Execution Engine - the InitializationEngine
- How the initialization system is started
- Early initialization and TerminateInitializationException - Creating an Initialization Module
- Logging
- Appendix
- IInitializableModule interface definition
MEF - The Discovery Mechanism
Locating the initialization modules in an EPiServer CMS application is achieved through utilization of Microsoft's Managed Extensibility Framework (MEF). MEF will be a part of .NET 4, though for EPiServer CMS 6 we ship a version of MEF which runs.NET 3.5.
MEF primarily uses an attribute-based discovery system. EPiServer CMS scans all assemblies loaded in the current AppDomain for initialization modules. This includes all assemblies in the bin folder, since by default the ASP.NET config section <system.web> / <compilation> / <assemblies> contains the configuration <add assembly="*"/>, this causes the ASP.NET build system to pull-in all assemblies in the bin folder.
In order to have a discoverable initialization module you need to add the [InitializableModule] or [ModuleDependency(...)] attribute to your class. The class also needs to implement the IInitializableModule interface.
In order to speed up the initialization process, the assembly scanning process supports some filtering logic which will reduce the number of assemblies that are scanned - see the filtering section in this document for further information.
(Note that the ASP.NET build system does not actually guarantee that all the assemblies defined by the <compilation> / <assemblies> configuration are actually loaded into the AppDomain when the web application starts executing. The loading of some assemblies can be delayed in order to speed up the display of the first request reaching ASP.NET. Unfortunately the EPiServer CMS initialization system is the first piece of code that we need to execute and therefore a small piece has been added to force all assemblies in the bin folder to be loaded before the discovery process starts.)
Dependency sorting
The same initialization code will be utilized in EPiServer CMS 6 and EPiServer Community 4 and also in future version of these platforms.
If your application has a dependency on one or more existing initialization modules, then you should explicitly state this dependency by using the [ModuleDependency(typeof(ModuleThatIDependOn))] attribute. This is used by the dependency sorting to ensure that all modules are executed in the correct order. Note that you can define multiple modules that the application is dependent upon.
The EPiServer.Web.InitializationModule sets up all EPiServer CMS internals in order for you to start using the EPiServer CMS API:s. If your module has no dependencies (which is an unlikely scenario) you can use the [InitializableModule] attribute.
The modules are then simply sorted, ensuring that the modules are executed in the correct dependency order.
If the application has defined circular dependencies or dependencies to non-existing modules then you will receive an exception upon application startup.
Execution Engine - InitializationEngine
InitializationEngine (resides in the namespace EPiServer.Framework.Initialization) is responsible for executing the list of modules created by the dependency sorting algorithm. As described in the IInitializableModule.Initialize method (see appendix) it is guaranteed that the Initialize method gets called only once (unless the method throws an exception).
The initialization engine is a simple state machine that will log information about all state changes as well as reporting the initialization methods that have been executed (see the logging section in this document for further information).
In the case of an exception the system will stop executing initialization code and simply wait for the next incoming request and then retry the failing Initialization method. This behavior will ensure that the system does not start up in a partially initialized state.
After all the Initialization methods have been executed successfully an InitComplete event will be raised. This is to allow you to perform post-initialization actions. For further information regarding this see the InitComplete event section in this document.
How the initialization system is started
The new initialization system is called by the constructor in the EPiServer.Global base class, i.e. the class that should be inherited from Global.asax.cs.
The reason the call to the initialization system is placed at that specific point is due to the fact that it is the first piece of code under EPiServer CMS's control that is invoked by ASP.NET.
(Please note that this will change with the release of IIS 7.5/.NET 4 where it will be possible to have "Always running" web applications - see Scott Guthrie's blog post regarding this.)
In the case of exceptions being thrown by a startup module EPiServer CMS will retry the initialization at the next BeginRequest event.
The major advantage of using the earlier initialization point is the fact that it is executed before Application_Start, which allows to have to have the application fully initialized and usable from within Application_Start (in most cases - see TerminateInitializationException).
Early initialization - TerminateInitializationException
When there are cases where EPiServer CMS does not have access to an incoming request one question that may be posed is how the application manages to identify the correct EPiServer site section in the web.config - in this instance the application cannot identify the correct EPiServer site section in the web.config - to meet this challenge a special technique is used:EPiServer CMS utilizes the TerminateInitializationException. By throwing this exception from within the Initialization method the application will stop execution of the initialization and the InitializationEngine will catch the exception to prevent an error message being displayed.
In the case of the built in SiteMappingConfiguration module this exception is being thrown the first time the EPiServer CMS 6 application executes (after installation). The execution will be resumed when the application reaches the BeginRequest event and the SiteMappingConfiguration will write the "Metabase path-to-siteId" mapping into the EPiServerFramework.config file. This information can then be used on future startups to determine which site section should be used prior to having an active HTTP request.
Creating an Initialization Module
It is important that when creating when your own initialization module that you add a reference to the System.ComponentModel.Composition.dll delievered by EPiServer (this assembly constitutes the core of the Managed Extensibility Framework and has been specially compiled by EPiServer).
In the simple Initialization code example below a property is set up with a default implementation in Initialize and then the process is undone in Uninitialize (this is the actual code from EPiServer.Data assembly):
[InitializableModule]
public class DataInitialization : IInitializableModule
{
public static DataInitialization Instance
{
get;
private set;
}
#region IInitializableModule Members
public void Initialize(InitializationEngine context)
{
DynamicDataStoreFactory.Instance =
new EPiServerDynamicDataStoreFactory();
Instance = this;
}
public void Uninitialize(InitializationEngine context)
{
Instance = null;
DynamicDataStoreFactory.Instance = null;
}
public void Preload(string[] parameters)
{
}
#endregion
}
A more complex example from the EPiServer.Framework assembly can be found below. The code below makes use of the TerminateInitializationException to postpone the rest of the initialization until application execution reaches the FirstBeginRequest event. At this point the entire Initialize method will be re-executed.
[InitializableModule]
public class SiteMappingConfiguration : IInitializableModule
{
//
//... Code deleted to focus on the initialization...
//
#region IInitializableModule Members
public void Initialize(InitializationEngine context)
{
var section = EPiServerFrameworkSection.Instance;
InitializeFromConfig(section.SiteHostMapping);
var configSiteId = SiteIdFromConfig(section);
// Get info from config file /
var actualSiteId = SiteIdFromRequest();
// Get info from HttpContext / Request / ServerVariables / host
// If both actual & config siteId are null, then we cannot
// determine siteId at this point in time
if (actualSiteId == null && configSiteId == null)
{
throw new TerminateInitializationException("Cannot determine
siteId at this time - wait for BeginRequest");
}
// If actual is null, that means we have info in
// configuration - just use it
if (actualSiteId == null)
{
SiteId = configSiteId;
}
// This clause handles the case when both config and actual has
// information, use actual and update config if different
else if (actualSiteId != configSiteId)
{
SaveSiteIdInConfig(actualSiteId);
SiteId = actualSiteId;
}
// Both actual and configured siteId are the same, just use it
else
{
SiteId = actualSiteId;
} Instance = this;
}
public void Uninitialize(InitializationEngine context)
{
Instance = null;
SiteId = null;
_hostNameToSiteLanguage = null;
_portWildcardExists = false;
_hostWildcardExists = false;
}
public void Preload(string[] parameters)
{
}
#endregion
Recommendations for your Initialization Module
- Allow for Initialize to be called multiple times
If you carry out multi-step initialization in your Initialize method and if it is re-executed because of an exception - ensure that the application handles this scenario correctly. A simple example is presented:
private bool _eventAttached;
public void Initialize(InitializationEngine context)
{
if (!_eventAttached)
{
SomeClass.AnEvent += MyEventHandler;
_eventAttached = true;
}
MethodThatMayThrowException();
}
This Initialize method may throw an exception after the event handler has been hooked up. The initialization system will re-invoke the Initialize method on the next request that reaches the web application and if the event hook-up is not protected with a flag it would get added again. - The initialization engine will make sure that your code executes in a single-threaded manner.
It is not necessary to lock regions when dealing with shared state etc. Note that this guarantee is only made for Initialize / Unintialize when executing through the initialization system. If you have custom code that makes any calls directly into your initialization module then you may need to deal with multi-threading issues. - Remember that the initialization system tracks the initialization state of your module.
- Expose your initialization module with a static Instance property.
One convention that we suggest you follow is to create a static Instance property that simply exposes the current instance of you initialization module. This gives your code an easy way to access the initialization module, otherwise it will be difficult to access since it is created from the initialization engine. - Do a full implementation of Uninitialize
Anything done by Initialize should be undone by Uninitialize. It should also be undone in the reverse order of Initialize. - Do not add logic to the Preload method
If you selet the option in Visual Studio to implement the IInitializableModule interface the Preload method will be generated as "throw new NotImplementedException();". The Preload method has been added to support the "Always running" concept in ASP.NET 4 (only works with IIS 7.5 and later), but since there is currently no way of testing this code (it is never called by the initialization system in EPiServer CMS 6) you should not attempt to implement it. In the EPiServer.Data initialization module the only thing we do on Initialize is to assign. - Use the System.ComponentModel.Composition.dll shipped by EPiServer.
Assembly scanning and filtering
When the initialization system looks for modules it relies on MEF to handle loading and composition. The basic idea is to scan all assemblies that are part of the application, with the exception of .NET Framework assemblies. This assembly list is what is exposed through the EPiServer.Framework.Initialization.InitializationModule.Assemblies static property.
The EPiServer CMS plug-in system now uses the same list of assemblies as the initialization system. This is important to remember when you decide which assemblies should be scanned by EPiServer Framework.
Built into the EPiServer Framework there is a filtering mechanism that is based on two different attributes, the [PreventAssembyScan] attribute and the [AllowAssemblyScan("Product")] attribute, as well as a configuration section.
Firstly the [PreventAssemblyScan] attribute, it is recommended that you add this attribute to your assembly if it does not contain any initialization modules nor any EPiServer CMS plugins. This attribute will help improve the startup time of your web application since scanning a large assembly is an expensive process.
Another option is to use the [AllowAssemblyScan("Product")] attribute. This is a slightly more complicated attribute to use since the purpose is also to exclude assemblies from the scanning process when possible, which may sound contrary to the name of the attribute.
An example should make this more clear:
You create an assembly MyCmsPlugins.dll that contains EPIServer CMS plugins. You want to avoid having EPiServer Community scan the assembly for performance reasons. Add the attribute [AllowAssemblyScan("CMS")] to MyCmsPlugin.dll. This will include the assembly when scanned by EPiServer CMS, but not when scanned by EPiServer Community.
Assuming a few Community extensions are added to the assembly, though you still want to limit scanning to only be done by EPiServer CMS and EPiServer Community, simply add the attribute[AllowAssemblyScan("Community")] in addition to [AllowAssemblyScan("CMS")].
Currently only two strings are defined "CMS" and "Community" to be used with [AllowAssemblyScan]. The "Community" part will be used by EPiServer Community 4 and "CMS" is used by EPiServer CMS 6.
Note that scanning for initialization modules is carried out regardless of the [AllowAssemblyScan] attribute, it is only honoured by product specific features - EPiServer Framework is cross-product.
Customizing assembly scanning with configuration
There are optional configuration settings that can be used to customize the assembly scanning process. These configuration settings are placed in the EPiServerFramework.config file. Note that the default configuration (see <configuration> / <configSections> in web.config) sets the restartOnExternalChanges attribute to false (changes to this file will not restart your web application).
In EPiServerFramework.config you will find a section:
<scanAssembly forceBinFolderScan="true" />
This section can be used to customize the assembly scanning process. It should be regarded as an additional filter on top of the assemblies normally loaded by ASP.NET as controlled by the <system.web> / <compilation> / <assemblies> section. Note that the bulk of the configuration usually resides in the systems web.config file.
If you want to exclude some specific assemblies from the normal scanning process as described in the previous section see the example below:
<scanAssembly forceBinFolderScan="true>
<add assembly="*" />
<remove assembly="MyAssembly" />
<remove assembly="MyAssembly2" />
</scanAssembly>
This will include all assemblies by virtue of the <add assembly="*" /> directive (except those filtered by attributes as described above) except for MyAssembly and MyAssembly2. The second mode of usage is to only scan specific assemblies by adding configuration similar to this:
<scanAssembly forceBinFolderScan="true>
<add assembly="EPiServer.Framework" />
<add assembly="EPiServer.Data" />
<add assembly="EPiServer.Events" />
<add assembly="EPiServer.Shell" />
<add assembly="EPiServer" />
<add assembly="EPiServer.Enterprise" />
<add assembly="EPiServer.Cms.Shell.UI" />
</scanAssembly>
This will exclude any other assemblies from being scanned. Note that the selection of assemblies above represent all assemblies delivered with EPiServer CMS 6 that have initialization modules (these assemblies must be present for EPiServer CMS 6 to work properly).
The InitComplete event
There are cases where you might want your own initialization module to be called again after the initialization process is complete. A typical use case (also featured in EPiServer Community 4) is to attach event handlers to an instance property that may be overridden by third party code.
To attach to the InitComplete event you could write your Initialize method as below:
public void Initialize(InitializationEngine context)
{
context.InitComplete += InitCompleteHandler;
StartThisModule();
}
When all initialization modules have executed the InitComplete event is raised. Note that the InitComplete event will be handled in a slightly non-standard way. When an event handler has executed without throwing an exception the initialization system will remove it from the InitComplete event. This means that you should not detach from the InitComplete event in your Uninitialize method.
This procedure in the initialization system may seem somewhat peculiar though it is simply to make sure that if an InitComplete event handler throws an exception, EPiServer CMS can re-execute the InitComplete event on the next request without re-invoking the already successfully executed event handlers.
Logging
Add the configuration below to episerverlog.config to enable logging of the initialization engine:
<logger name="EPiServer.Framework.Initialization">
<level value="All" />
<appender-ref ref="fileLogAppender" />
</logger>
Appendix
See below for the IInitializableModule interface definition:
namespace EPiServer.Framework
{ /// <summary>
/// Interface that you can implement to be part of the EPiServer Framework
/// initialization chain.
/// </summary>
/// <remarks>
/// You should set the attribute [InitializableModule] on the class
/// implementing this interface, or if you want to control dependencies
/// (making sure that other modules are initialized before your module gets called)
/// use the attribute [ModuleDependency(typeof(ClassThatIDependOn), ...)].
/// </remarks>
public interface IInitializableModule
{
/// <summary>
/// Initializes this instance.
/// </summary>
/// <param name="context">The context.</param>
/// <remarks>
/// <para>
/// Gets called as part of the EPiServer Framework initialization sequence.
/// Note that it will be called only once per AppDomain, unless the method
/// throws an exception. If an exception is thrown, the initialization
/// method will be called repeatedly for each request reaching the site
/// until the method succeeds.
/// </para>
/// <para>
/// The "called once" guarantee uses the IsIntialized property as defined on
/// this interface. The value of this property will be set by the EPiServer
/// Framework initialization system and you should not set it directly.
/// </para>
/// </remarks>
void Initialize(InitializationEngine context);
/// <summary>
/// Resets the module into an uninitialized state.
/// </summary>
/// <param name="context">The context.</param>
/// <remarks>
/// <para>
/// This method is usually not called when running under a web application
/// since the web app may be shut down very abruptly, but your module
/// should still implement it properly since it will make integration and
/// unit testing much simpler.
/// </para>
/// <para>
/// Any work done by <see cref="Initialize"/> as well as any code executing
/// on <see cref="InitializationEngine.InitComplete"/> should be reversed.
/// </para>
/// </remarks>
void Uninitialize(InitializationEngine context);
/// <summary>
/// Preloads the module.
/// </summary>
/// <param name="parameters">The parameters.</param>
/// <remarks>
/// This method is only available to be compatible with "AlwaysRunning"
/// applications in .NET 4 / IIS 7. It currently serves no purpose.
/// </remarks>
void Preload(string[] parameters);
} }