Introducing the new Initialization System in EPiServer CMS 6
[NOTE - this article has been edited to show changes introduced after RC1.
Strikethrough is used to indicate things no longer relevant after RC1]
For some background I suggest that you read the previous blog post in the series http://world.episerver.com/Blogs/Magnus-Strale/Dates/2009/12/Initialization-in-EPiServer-CMS-5/
This post describes what the initialization system is and how it works. How to implement your own initialization module will be the subject of a future blog post.
Why a new initialization system?
There are several ways to initialize modules in EPiServer CMS 5, but none were really designed as a general-purpose initialization system. The new initialization system (resides in assembly EPiServer.Framework, namespace EPiServer.Framework.Initialization) 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.
What is it?
It is basically four things:
- 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.
- Hooks into ASP.NET to handle re-execution of initialization modules in the face of exceptions during startup.
The discovery mechanism
Locating the initialization modules is done by making use of Microsoft's new Managed Extensibility Framework (aka MEF). MEF will be a part of .NET 4, but for EPiServer CMS 6 we will ship a version running on .NET 3.5.
MEF primarily uses an attribute-based discovery system. We will scan all assemblies loaded in the current AppDomain for initialization modules. This will include all assemblies in the bin folder, since by default the ASP.NET config section <system.web> / <compilation> / <assemblies> contains a line <add assembly="*"/>. This causes the ASP.NET build system to pull in all assemblies in the bin folder.
Interesting note: The ASP.NET build system does not actually guarantee that all the assemblies defined by the <compilation> / <assemblies> are actually loaded into the AppDomain when your 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 initialization system is the first piece of code that we want to execute and therefore we have added a small piece of code to force all assemblies in the bin folder to be loaded before the discovery process starts.
In the future we plan to to the same for all assemblies referenced by the <compilation> / <assemblies> section. This would allow us to support plugins / Initialization modules loaded purely from the GAC without having to keep the file in the bin folder as well. Unfortunately this feature did not make it in time for EPiServer CMS 6 code freeze.
In order to have a discoverable initialization module you need to add the [InitializableModule] or [ModuleDependency(...)] attribute to your class and the class should also implement the IInitializableModule interface.
The assembly scanning process supports some filtering logic to reduce the number of assemblies that we scan in order to speed up the initialization process. I will return to the filtering in the third post in this series.
The IInitializableModule interface
This interface is defined like this:
/// Interface that you can implement to be part of the EPiServer Framework initialization chain.
/// 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), ...)].
public interface IInitializableModule
/// Initializes this instance.
/// <param name="context">The context.</param>
/// 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.
/// 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.
void Initialize(InitializationEngine context);
/// Resets the module into an uninitialized state.
/// <param name="context">The context.</param>
/// 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.
/// Any work done by <see cref="Initialize"/> as well as any code executing on <see cref="InitializationEngine.InitComplete"/> should be reversed.
void Uninitialize(InitializationEngine context);
/// Gets or sets a value indicating whether this instance is initialized.
/// <c>true</c> if this instance is initialized; otherwise, <c>false</c>.
/// You should usually not set this property from your code. The defautl state should be false and then let
/// EPiServer Framework initialization set it for you when the initialization is done. The reason for not
/// setting it from your code is that EPiServer framework will execute in a locked region to avoid concurrency
/// issues when updating the state of this property. This lock is not available outside the initialization system.
/// Preloads the module.
/// <param name="parameters">The parameters.</param>
/// This method is only available to be compatible with "AlwaysRunning" applications in .NET 4 / IIS 7.
/// It currently serves no purpose.
void Preload(string parameters);
The comments contains most of what is worth mentioning.
Those of you that are familiar with EPiServer Community will probably recognize this discussion. The initialization system in EPiServer Framework has used a lot of the ideas in the EPiServer Community initialization system and merged the code so that EPiServer CMS 6 and EPiServer Community 4 will use the same initialization code. This should improve startup time for Relate+ systems (or any web application using both EPiServer CMS and EPiServer Community).
If you have 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 right order. Note that you can define multiple modules that you depend on.
If your module have no dependencies, which is an unlikely scenario, you can use the [InitializableModule] attribute. Most likely you will at least have a dependency on EPiServer.Web.InitializationModule which sets up all EPiServer CMS internals in order for you to start using the EPiServer CMS API:s.
The modules are then simply sorted so as to guarantee that the modules are executed in dependency order. If you have defined circular dependencies or dependency to a non-existing module you will receive an exception upon startup.
A possible future optimization that I would like to see (don't know it it is going to happen) is to create a dependency graph instead of a sorted list and execute the independent branches in parallel.
The execution engine
...which is actually named InitializationEngine is responsible for executing the list of modules created by the dependency sorting algorithm. As described on the IInitializableModule.Initialize method (see above) we guarantee that the Initialize method gets called once and 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 has been executed.
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 make sure that the system does not start up in a partially initialized state.
After all the Initialization methods has been executed successfully an InitComplete event will be raised. This is to allow you to perform post-initialization actions. More details on this in the upcoming "Advanced topics" blog post.
How do we start the initialization system?
The new initialization system is called by the constructor in the EPiServer.Global base class, i e the class that you should inherit from in Global.asax.cs.
The reason we have placed the call to the initialization system at that specific point is simply that it is the first piece of code under our control that is invoked by ASP.NET.
Note that this will change with IIS 7.5/.NET 4 where you can have "Always running" web applications. See Scott Guthrie's blog post http://weblogs.asp.net/scottgu/archive/2009/09/15/auto-start-asp-net-applications-vs-2010-and-net-4-0-series.aspx
In the case of exceptions being thrown by a startup module we will retry the initialization at next BeginRequest event, which is basically the same model as we used for EPiServer CMS 5.
The big advantage of using the earlier initialization point is the fact that it is executed before Application_Start, which will allow us (in most cases - see the next paragraph) to have EPiServer CMS fully initialized and usable from within Application_Start.
Early initialization gotchas
If you remember information from http://world.episerver.com/Blogs/Magnus-Strale/Dates/2009/12/Initialization-in-EPiServer-CMS-5/ you might ask yourself how we can identify the correct EPiServer site section in web.config when there are cases where we do not have access to an incoming request.
Well, we can't.
This is where the TerminateInitializationException comes to the rescue. By throwing this exception from within your Initialization method you will stop execution of the initialization but 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 your EPiServer CMS 6 application executes after installation. The execution will be resumed when we reach 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.
I think that will be enough for today. Stay tuned for more details and actual "How-to" information for writing your own initialization module.