Views: 11487
Number of votes: 1
Average rating:

The new initialization system - How to write an initialization module and advanced topics

Here is the third part of the blog series about initialization in EPiServer CMS. For background you may want to read http://world.episerver.com/Blogs/Magnus-Strale/Dates/2009/12/Initialization-in-EPiServer-CMS-5/ and http://world.episerver.com/Blogs/Magnus-Strale/Dates/2009/12/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]

Writing your own initialization module

This should usually be a very straight-forward process. I will start by showing two different examples, one very simple and one more complex. The code shows most of the recommendations outlined later in this post. To start with the very simple initialization (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 bool IsInitialized
    {
        get;
        set;
    }

    public void Preload(string[] parameters)
    {
        throw new NotImplementedException();
    }
    #endregion
}

In the code above we just set up a property with a default implementation in Initialize and undo the process in Uninitialize.

A somewhat more complex example (code snippet from the EPiServer.Framework assembly):

[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 bool IsInitialized
     {
         get;
         set;
     }


     public void Preload(string[] parameters)
     {
         throw new NotImplementedException();
     }

     #endregion

The code above makes use of the TerminateInitializationException to postpone the rest of the initialization until we reach the FirstBeginRequest event. At that point the entire Initialize method will be re-executed.

Recommendations for your Initialization Module

  1. Allow for Initialize to be called multiple times
    If you do multi-step initialization in your Initialize method, make sure that it will do the correct thing if it is re-executed because of an exception. A very simple example:
    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 next request that reaches the web application and if we did not protect the event hook-up with a flag it would get added again.
  1. The initialization engine will make sure that your code executes in a single-threaded manner.
    No need for you to lock regions when dealing with shared state etc. Note that this guarantee is only made for Initialize / Unintialize when executing thru 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.
  2. Don't modify the IsInitialized property
    Let the initialization system set the value of this property rather than modifying it yourself. Otherwise you may cause the initialization system to malfunction.
    The initialization system tracks the initialization state of your module.
  3. Expose your initialization module with a static Instance property.
    A convention that I 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 hard to get at since it is created from the initialization engine.
  4. Do a full implementation of Uninitialize
    Anything done by Initialize should be undone by Uninitialize. It should also be un-done in the reverse order of Initialize.
  5. Do not add logic to the Preload method
    If you let Visual Studio implement the IInitializableModule interface the Preload method will be generated as "throw new NotImplementedException();". Please leave it like that. 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. Note that it looks like we break this recommendation with our first example of an initialization module. In the EPiServer.Data initialization module the only thing we do on Initialize is to assign

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 thru the EPiServer.Framework.Initialization.InitializationModule.Assemblies static property.

The EPiServer CMS plugin system has been updated to use 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 we have a filtering mechanism that is based on two different attributes, the [PreventAssembyScan] attribute and the [AllowAssemblyScan("Product")] attribute, as well as a configuration section.

To start with the simple part it is quite obvious what [PreventAssemblyScan] will do. 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. It is a bit more complicated 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 hopefully make it 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 that the assembly gets the addition of a few Community extensions, but 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 we only define the two strings "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 done regardless of the [AllowAssemblyScan] attribute, it is only honoured by product specific features and EPiServer Framework is cross-product.

Customizing assembly scanning with configuration

EPiServer is trying to reduce the amount of configuration as much as possible, but there are still optional configuration settings that you can use to customize the assembly scanning process. This is placed in the EPiServerFramework.config file. Note that the default configuration (see <configuration> / <configSections> in web.config) sets the restartOnExternalChanges attribute to false. I e 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 do the following:

<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 has an initialization modules. I e these assemblies must be present for EPiServer CMS 6 to work properly.

The InitComplete event

There are cases where you might want you initialization module to be called again after the initialization process is complete. A typical use case (borrowed from EPiServer Community 4) is attaching 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 like this:


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.

Why does the initialization system do such a strange thing? Simply to make sure that if an InitComplete event handler throws an exception, we can re-execute the InitComplete event on next request without re-invoking the already successfully executed event handlers.

Summary

This should hopefully explain most of the features of our new initialization system as well as how to use it. Please send feedback both on the feature itself and if there is anything in the blog posts that is not clear and I will try to fill in the gaps.

I hope that with this new system in place we can provide an even better platform for you to build and improve upon!

Dec 21, 2009

Guest
(By Guest, 9/21/2010 12:32:59 PM)

Great article. I have a few questions that maybe you can clarify.

Firstly, if the IsInitialized method must not be modified by the owning module and should have no behavior, does it really belong in the IInitializableModule interface? To me it breaks the principle of least surprise. If I were to implement that interface without reading this article, I would have set the property to true on initialization and false on uninitialization, which as you said could have bad consequences. Maybe I am missing something, but it seems to me that if the initialization engine needs to keep track of what modules are initialized, it should maintain that data itself - not force it upon the modules.

Secondly, now that you ship EPiServer with a sort of IoC container (MEF), why not take the opportunity to stop using the coupling-inducing and test-hindering singleton pattern? Instead of a singleton property, you could add each module to MEF after initialization. People could then get the instance through dependency injection or by using MEF as a service locator. That would also mean less repetitive boilerplate code for IInitializableModule implementors. The same goes for the DynamicDataStore.Instance, why not add (I)DynamicDataStore to the MEF-container instead of creating yet another singleton?

With kind regards
Patrik Akselsson

/ Patrik Akselsson

ms
(By ms, 9/21/2010 12:32:59 PM)

Excellent comments Patrik.

Regarding IsInitialized it started out as a module-controlled flag to allow it to be set when the module itself was in a "Done" state, but the "retry-on-exception" approach turned out to be a more robust mechanism. In the end we simply did not re-examine the interface carefully enough in face of the refactorings we made.

You are right, the IsInitialized flag no longer belongs in the IInitializableModule interface, unfortunately we have reached code freeze for CMS 6...

Re IoC: We will absolutely move to an IoC model, but the changes were deemed to extensive to implement for CMS 6. After CMS 6 has shipped we will start doing some serious refactorings.

Thank you for taking the time to give valuable feedback!

Magnus Stråle

Vlad Rudkovskiy
(By Vlad Rudkovskiy, 9/21/2010 12:32:59 PM)

Good article! In addition to EPiServer assemblies to scan can you publish similar list for Community 3.2?

ms
(By ms, 9/21/2010 12:33:00 PM)

Community 3.2 does not use the same initialization system (although many features looks very similar), it will be introduced with Community 4, i e the assembly list would not make sense for 3.2.

Please login to comment.