Utilising MEF to self-register HTTP modules

by Tom Pipe 16. January 2012 22:08

After reading about .NET 4’s new PreApplicationStart method being used to register build providers without touching the web.config. I thought about other instances where editing the web.config was mundane. The first thing that came to mind was registering HTTP modules.

HTTP Modules are a fantastic way to intercept HTTP requests, and “inject” code at each stage of the request lifecycle, and they are invaluable for sectioning code into pluggable components for reuse in another application. The only thing that’s bugged me is the need to register each module by adding an entry in the <httpModules> section within the web.config.

Clearly great minds think alike. David Ebbo, an architect on the ASP.NET team announced a RegisterModule() API had been added in MVC3 allowing HttpModules to be registered without touching config, by using code like below:

    [assembly: System.Web.PreApplicationStartMethod(typeof(Initializer), "Initialize")]
    public static class Initializer
    {
        public static void Initialize()
        {
            DynamicModuleUtility.RegisterModule(typeof(ModuleA));
            DynamicModuleUtility.RegisterModule(typeof(ModuleB));
            DynamicModuleUtility.RegisterModule(typeof(ModuleC));
        }
    }

Yes it has it’s advantages, but this feels more like a step sideways than a step forward. Each module still has to be explicitly defined, so it’s almost exactly the same steps as adding a HttpModule via the web.config, albeit a slightly different syntax.

This got me wondering if there was an easy way to discover modules and automatically register them using the RegisterModule method, and that seemed like a perfect scenario for MEF.

Managed Extensibility Framework (MEF)

MEF is a component of .NET 4.0 for creating lightweight, extensible applications. Extensions (known as parts, or exports) can be dynamically discovered (imported) from a configurable location (known as a Catalog) and used at run-time with no configuration required.

MEF uses an attributed programming model where “imports” and “exports” are declared by decorating classes or members with the Import and Export attributes.

An Import is matched with one or more Exports, providing they have the same “contract”. The contract consists of a string, called the contract name, and the type of the exported or imported object, called the contract type. Only if both the contract name and contract type match is an export considered to be a match.

Attributes

Usually classes are marked with the ExportAttribute but I wanted something that’s more distinct and self-explanatory. Therefore I created a new attribute, derived from the standard ExportAttribute. The code for the attribute is as follows:

    [MetadataAttribute]
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    public class AutoRegisterHttpModuleAttribute : ExportAttribute, IMefMetaData
    {
        public AutoRegisterHttpModuleAttribute() : base(typeof(IHttpModule))
        {
            Enabled = true;
        }

        public bool Enabled
        {
            get; set;
        }

        public string Name
        {
            get; set;
        }
    }

Nothing unusual here, except the MetadataAttribute. This informs MEF that there is additional metadata to be exposed. There isn’t necessarily a requirement for any metadata, but I’ve taken the opportunity to add some basic properties to assist us (I’ll describe this later).

The metadata is abstracted to an interface named IMefMetaData , known as a metadata view. A metadata view must have only properties, and those properties must only have get accessors.

    public interface IMefMetaData
    {
        bool Enabled
        {
            get;
        }

        string Name
        {
            get;
        }
    }

Back to the attribute, note the Type parameter passed into the base constructor, this just defines the Type of our contract stating it must be of type IHttpModule.

Now we have an attribute with which to decorate our HttpModules, we need to instruct MEF to discover and import any classes marked with this attribute and compose them into a Catalog.

Discovery and Import

As mentioned above, a MEF contract is just a string (when importing by type, it’s just the name of that type). There are two ways you can define an Import in MEF, Import and ImportMany with cardinality being the difference.

The Import attribute refers only to single imports. (zero-to-one and exactly-one cardinalities). Marking a property with this attribute specifies there is a single instance of the stated contract, and there is a single source to match.

Using ImportMany attribute declares zero-to-many cardinality, and states that instead of a single export, there can be many exports which match the contract. Using it on an IEnumerable, as shown below, MEF will fill the IEnumerable with all parts it can find that export that type, in this case IHttpModule.

        [ImportMany]
        public IEnumerable<IHttpModule> Modules { get; set; }

Usually this is sufficient, but because we want to use some metadata, we need to alter the property signature in order to expose it. MEF provides an overload of Lazy<T> to allow access to the metadata without instantiating the underlying export. The corresponding property signature looks like this:

        [ImportMany(typeof(IHttpModule))]
        public IEnumerable<Lazy<IHttpModule, IMefMetaData>> Modules
        {
            get; set;
        }

The property alone won’t do a thing, we need to instruct MEF to discover parts and populate this property. Firstly we tell MEF we want to discover attributed parts within the application’s bin directory by using the DirectoryCatalogclass. Next we create a CompositionContainer to hold the parts for composition and finally we tell MEF to Compose the Import parts within this class:

        var catalog = new DirectoryCatalog(HttpRuntime.BinDirectory);
        var container = new CompositionContainer(catalog);
        container.ComposeParts(this);

This populates the Modules property with all classes decorated with the attribute we defined earlier, so we should now have an IEnumerable list of HttpModules that need to be auto-registered, so we now just need to call the RegisterModule method for each module:

        foreach (var httpModule in this.Modules.Where(m => m.Metadata.Enabled))
        {
            var msg = httpModule.Metadata.Name ?? httpModule.GetType().FullName;

            log.InfoFormat("Adding SelfRegistering HttpModule {0}", msg);
            DynamicModuleUtility.RegisterModule(httpModule.Value.GetType());
        }

So by utilising the metadata added earlier, we can add some rudimentary notifications using log4net, showing which modules are being registered (helpful for debugging) and we also have a facility to easily disable modules in code to also assist in debugging etc.

Conclusion

So now instead of registering a HttpModule via the web.config, we simply decorate it with the AutoRegisterHttpModuleAttribute. It will get automatically registered and will intercept any requests as normal.

In my opinion this “shortcut” is an absolute godsend for distributing self-contained projects and sharing code between solutions with minimal configuration - HttpModules can now really be the “plug-ins” I think they were intended to be.

Gotchas

I foresee only one issue whereby it could take other developers a while to find and debug any issues if they are being caused by a self-registered HttpModule. However once we become accustomed to using this method of registering modules, it should be easy enough to find modules using the “Find usages” feature within Visual Studio. A minor issue compared to the benefits this offers.

Future ideas and Code download

I will shortly be uploading the full VS solution (probably to EPiCode), but first I want to add a few useful additions in there that utilise this technique, and I’ll write some more blog posts explaining those awesome plugins.

Watch this space. Smile

Tags:

.Net | HttpModule | MEF

CMS Tab Captions

by Tom Pipe 23. November 2011 15:44

By default EPiServer CMS creates some “standard” tabs. Here’s a screenshot of the DB showing the default captions etc:

clip_image001

However, the GroupCaption is NOT the caption which is displayed in the CMS, that is taken from the language XML file. Here’s a snippet from the LanguageEN.xml file showing the English translations:

image

Whilst most of the tabs translations are identical, the Information tab will display “Content“ as it’s caption, not “Information”.

Take care not to create a tab with a caption of “Content” or “Information”, or you could create a confusing situation where two identically names tabs appear in the CMS. e.g:

clip_image002

Which can result in this:

image

This can confuse some non savvy editors. Try not to make things difficult for them Smile

Tags:

EPiServer | CMS | Translation

EPiServer Community and image thumbnailing woes

by Tom Pipe 23. September 2011 20:40

The task…
The EPiServer community product has an integrated ImageGallery Module which handles persisting and retrieval of Images within the system. The module also offers a handy image thumbnailing feature via a number of overloaded methods within the ImageGalleryHandler class.

The thumbnailing is controlled by some settings within the web.config file, as shown below:

<imageGallery 
    imageAbsoluteFilePath="C:\Site\webRoot\ImageGallery\Originals" 
    thumbnailVirtualFilePath="~/ImageGallery/Thumbnails" 
    imgExtension=".jpg" 
    imgMaxWidth="640" 
    imgMaxHeight="640" 
    saveOriginal="true" 
    maxUserImageQuota="0" 
    imgQuality="100">
</imageGallery>

These settings appear to allow the location of the images to be changed, which is all well and good in theory, but in practice it restricts the location of the thumbnails to always reside beneath the site root.

Due to our release process (a full release package, and new, dated IIS root folder for each release) the images HAD to reside outside the site root.

The answer…
Easy right? This is a perfect opportunity to use a Virtual Path Provider, isn't this exactly what they were designed for? EPiServer offer easy registration of various provider types within the episerver.config file and a boat load of custom providers within the EPiServer.Web.Hosting namespace. Hell, they even use them to relocate the UI and Util directories from within the program files directory.

So, registering the following provider within episerver.config should solve the issue right?

<add virtualPath="~/ImageGallery/" 
     physicalPath="C:\Site\ImageGallery"
     name="Galleries"
     type="EPiServer.Web.Hosting.VirtualPathNativeProvider,EPiServer" />


Unfortunately not.

The issue…
The thumbnail is actually created within the ImageGalleryFactory by an aptly named CreateThumbnail method.

Within the method body is the following if/else statement:

if (HostingEnvironment.IsHosted)
{
    absoluteImageGalleryPath = HostingEnvironment.MapPath(ImageGalleryModule.GetVirtualThumbnailPath(image.ImageGalleryId));
}
else
{
    absoluteImageGalleryPath = ImageGalleryModule.GetAbsoluteImageGalleryPath(image.ImageGalleryId);
}

GetVirtualThumbnailPath concatenates the ImageGalleryID onto the thumbnailVirtualPath specified in the web.config and in this example returns something like "~/ImageGallery/Thumbnails/8/8"

HostingEnvironment.MapPath therefore returns the physical path of the folder, as if it resided under the site root "C:\Site\webRoot\ImageGallery\Thumbnails".

The VPP we configured earlier is not taken into consideration, and thumbnails will always be created under the site root regardless.

The solution….
My original intention was to simply define a custom factory derived from ImageGalleryFactory and override the CreateThumbnail method. Unfortunately someone at EPiServer decided we shouldn't be allowed to customise the ImageFactory, and marked the class as internal, so that idea could progress no further.

To workaround this, I ended up creating a custom ImageGalleryHandler, derived from the standard ImageGalleryHandler. Defined a new static Instance property (getting and setting the underlying ImageGallery.Instance, cast to the custom type) which gets instantiated within the static constructor, and therefore overwrites the base.Instance.

Because it's a derived version of the ImageGalleryHandler, the instance can be passed around as a standard ImageGalleryHandler, and will appear to be so to other libraries and classes, which can use it as "normal". Polymorphism at its most useful!

I then created an override to the GetThumbnail method and within it, called the base.GetThumbnail overload, then if the createThumbnail parameter is true. I raise the OnThumbnailCreated event passing in the created thumbnail.

I subscribed to the OnThumbnailCreated event within the Application_Start in Global.asax, and within the event handler I use HostingEnvironment.VirtualPathProvider.GetDirectory method instead of Server.MapPath. This returns a NativeDirectory object which has a LocalPath property containing the physical path to the intended thumbnail folder as defined in the episerver.config.

I now have a reference to the new thumbnail object (and therefore the folder beneath the site root), and i also have a reference to the intended physical location. So after some brief sanity checks I can copy (or move) the new thumbnail to the destination folder.

OBJECTIVE COMPLETE Laughing

Conclusion…
Whilst probably not the most elegant of solutions, it definitely achieves the goal. I consider this to be a 'filthy hack' to workaround the limitations imposed by EPiServer's internal classes. If anyone has an suggestions, alternative methods, or improvements – feel free to discuss them below.

Within the source I've taken the idea further by subscribing to the ImageRemoved event exposed by the standard ImageGalleryHandler so I can "tidy up" the thumbnails when the original image is removed.

Update - 25/09/2011 23:16
Added an additional sanity check to create a subdirectory under the VPP folder if it doesn't already exist.

Source

namespace Custom.Community.Handlers
{
    using System;
    using System.Drawing;
    using System.IO;
    using System.Linq;
    using System.Web.Hosting;

    using EPiServer.Common;
    using EPiServer.Common.Exceptions;
    using EPiServer.Community.ImageGallery;
    using EPiServer.Web.Hosting;

    using Image = EPiServer.Community.ImageGallery.Image;

    public class CustomImageGalleryHandler : ImageGalleryHandler
    {
        static CustomImageGalleryHandler()
        {
            Instance = new CustomImageGalleryHandler();
        }

        protected CustomImageGalleryHandler()
        {
        }

        public event EPiServerCommonEventHandler ImageThumbnailCreated;

        public static new CustomImageGalleryHandler Instance
        {
            get
            {
                return (CustomImageGalleryHandler)ImageGalleryHandler.Instance;
            }
            set
            {
                ImageGalleryHandler.Instance = value;
            }
        }

        public static void Instance_ImageRemoved(string sender, EPiServerCommonEventArgs e)
        {
            var image = (Image)e.Object;
            string path = GetThumbnailVPP(image.ImageGallery).LocalPath;

            try
            {
                if (Directory.Exists(path))
                {
                    foreach (string str2 in Directory.GetFiles(path, image.ID + "_*").Where(File.Exists))
                    {
                        File.Delete(str2);
                    }
                }
            }
            catch (Exception exception3)
            {
                throw new FrameworkException("Could not delete thumbnails", exception3);
            }
        }

        public static void ThumbnailCreated(string sender, EPiServerCommonEventArgs e)
        {
            var thumb = (Thumbnail)e.Object;
            var vpp = GetThumbnailVPP(thumb.Parent.ImageGallery);

            if (!Directory.Exists(vpp.LocalPath))
            {
                try
                {
                    Directory.CreateDirectory(vpp.LocalPath);
                }
                catch (Exception exception2)
                {
                    throw new FrameworkException("Failed to create directory: \"" + vpp.LocalPath + "\".", exception2);
                }
            }

            var imagePath = HostingEnvironment.MapPath("~" + thumb.Url);
            if (string.IsNullOrEmpty(imagePath)) return;

            var source = new FileInfo(imagePath);
            var filename = Path.GetFileName(source.FullName);

            if (source.Exists && !string.IsNullOrEmpty(filename))
            {
                var dest = new FileInfo(Path.Combine(vpp.LocalPath, filename));
                if (!dest.Exists || source.LastWriteTime > dest.LastWriteTime)
                {
                    source.CopyTo(dest.FullName, true);
                }
            }
        }

        public override Thumbnail GetThumbnail(Image image, int width, int height, ThumbnailFormat thumbnailFormat, bool createThumbnail, Watermark thumbnailTag, Color thumbBgColor)
        {
            var thumbnail = base.GetThumbnail(image, width, height, thumbnailFormat, createThumbnail, thumbnailTag, thumbBgColor);

            if (createThumbnail)
            {
                this.OnThumbnailCreated(thumbnail);
            }

            return thumbnail;
        }

        protected virtual void OnThumbnailCreated(Thumbnail thumb)
        {
            if (this.ImageThumbnailCreated != null)
            {
                this.ImageThumbnailCreated(ImageGalleryModule.Instance.UniqueName, new EPiServerCommonEventArgs(thumb.ID.ToString(), "created_thumbnail", thumb));
            }
        }

        private static NativeDirectory GetThumbnailVPP(ImageGallery gallery)
        {
            try
            {
                var virtualPath = ImageGalleryModule.GetVirtualThumbnailPath(gallery.ID);
                var vpp = HostingEnvironment.VirtualPathProvider.GetDirectory(virtualPath) as NativeDirectory;

                if (vpp == null)
{ var dir = UnifiedDirectory.CreateDirectory(virtualPath.TrimStart('~')); vpp = dir
as NativeDirectory;
}

                if (vpp != null)
                {
                    return vpp;
                }

                throw new ArgumentNullException(null, string.Format("No provider configured for the {0} virtual path", virtualPath));
            }
            catch (Exception exception2)
            {
                throw new FrameworkException("Could not get path to upload directory", exception2);
            }
        }
    }
}

Tags:

EPiServer | CMS | Community | Relate

About the author

I'm Tom Pipe and I'm a .net developer at twentysix, an award winning digital agency based in Leeds and EPiServer's UK partner of the year 2010.

I'm passionate about my work, and have contributed towards websites, intranets and applications for UK government agencies and massive global brands.

Tag cloud