Click here to Skip to main content
15,887,596 members
Articles / Web Development / ASP.NET

Extensible Web Application

Rate me:
Please Sign up or sign in to vote.
4.86/5 (15 votes)
20 Oct 2013CPOL8 min read 20.8K   180   32   3
Howto replace existing ASP.NET pages using MEF based plugin solution...

Introduction

In the company I work for, we have a large web application, containing over 250 pages. Every time one of our customers has a new feature on his mind we figure out a price for it and develop. However, after developing we add the new feature to our common code-base and make it available to all of our customers, not only the on requested, and paid for it.

A few months ago a potential customer came to us and said that he wants to add another layer to this development-distribution schema. He believes he has some secret and powerful solutions, others have not. He wants a platform to add these solutions to our application by replacing certain pages. He also wants to keep these gems only for himself.

As today the only way, in our application, to replace certain pages is – to replace it. For obvious reasons of maintenance we do not want to keep two (or more) different applications, so I looked for a solution that can fit in an existing application.

After learning the issue I came up with some pre-requests:

  • Single file deployment – to replace any page the customer will put a single file into a predefined folder
  • To declare a page to be replaceable, we should do the minimum changes to our existing code
  • We should declare a simple and straight way to the customer's developer to declare a page to replace existing one (mostly to save us from debugging others code)
Note: When reading this article, remember that the solution made to fit into 
an existing application, and for that reason it's not an one-for-all, but rather a case-study.

In the rest of this article I will try to show and explain the solution I came up with…

Using the code

The code presented in the text body will not make a complete solution. To see a more complete implementation of the ideas in this article download and study the attached demo.

Points of Interest

The solution in this article uses two little gems of the ASP.NET jewelry.

MEF

"The Managed Extensibility Framework (MEF) is a composition layer for .NET that improves the flexibility, maintainability and testability of large applications. MEF can be used for third-party plugin extensibility, or it can bring the benefits of a loosely-coupled plugin-like architecture to regular applications." (http://mef.codeplex.com)

VirtualPathProvider

"The VirtualPathProvider class provides a set of methods for implementing a virtual file system for a Web application. In a virtual file system, the files and directories are managed by a data store other than the file system provided by the server's operating system. For example, you can use a virtual file system to store content in a SQL Server database." (http://msdn.microsoft.com/en-us/library/system.web.hosting.virtualpathprovider.aspx

Both these topics are worth study by on there own...

The Steps 

There are four main steps involved in the solution

  1. Create an extension point, means to declare an existing page in an application as one can be replaced by plugin.
  2. Create a plugin page. This page the one can replace the original.
  3. Load and select the plugin for a selected extension point.
  4. Render the loaded page to the client.

Extension Point

C#
public class ExtensiblePage : Page
{
  public virtual string ExtensionPoint
  {
    get;
    set;
  }
} 
public partial class Login : ExtensiblePage
{
  public override string ExtensionPoint
  {
    get
    {
      return ( "Login" );
    }
  }
} 

Using these few line of codes I declare the Login page as one can be replaced by plugin. The string itself will serve me to find the plugin pages intent to replace this one...

Plugin Page

C#
[InheritedExport]
public interface IPagePlugin
{
  Assembly Assembly
  {
    get;
  }
  
  string ResourceID
  {
    get;
  }
}
 
public interface IPagePluginMetadata
{
  string ExtensionPoint
  {
    get;
  }
  
  string PluginName
  {
    get;
  }
}
 
[MetadataAttribute]
[AttributeUsage( AttributeTargets.Class | 
  AttributeTargets.Interface, AllowMultiple = false, Inherited = true )]
public class PagePluginMetadataAttribute : ExportAttribute, IPagePluginMetadata
{
  public PagePluginMetadataAttribute ( )
    : base( typeof( IPagePlugin ) )
  {
  }
  
  #region IPagePluginMetadata Members
  
  public string ExtensionPoint
  {
    get;
    set;
  }
  
  public string PluginName
  {
    get;
    set;
  }
  
  #endregion
} 

The IPagePlugin interface has two purposes. One to declare the page implements it as loadable by MEF engine (InheritedExport attribute does it). The other is to provide information about this plugin, namely the assembly and resource info used to load the embedded page.

The IPagePluginMetadata, implemented by PagePluginMetadata attribute used to add metadata information to the plugin. In my case the info declares the extension point and adds a name - that can be used in configuration - to the plugin.

Now let see, how to use these interfaces...

C#
[PagePluginMetadata( ExtensionPoint = "Login", PluginName = "LoginExtension" )]
public partial class LoginExtension : Page, IPagePlugin
{
  #region IPluginPage Members
  
  public Assembly Assembly
  {
    get
    {
      return ( GetType ( ).Assembly );
    }
  }
  
  public string ResourceID
  {
    get
    {
      return ( string.Format ( "{0}.aspx", GetType ( ).FullName ) );
    }
  }
  
  #endregion
} 

This sample declares the LoginExension page as one can replace the extensible page with extension point "Login". Assembly and ResourceID properties return values can identify the page itself as embedded resource.

NOTE: I always speak about embedded resource. It's not part of the code provided in the text body (it's can be set in the IDE) but its important to remember that the plugin page is declared as embedded resource, and that is to fulfil the first pre-request I made at the beginning... 

Load Plugin Pages

MEF ways to load extensions is extremely powerful and makes it really fast-forward to get a list of available classes. MEF does it by scanning a specific (or more) folder for assemblies have classes decorated with selected interfaces in it. In my case the class must inherit the IPluginPage interface and be decorated by PagePluginMetadata.

C#
public class PagePluginCollection
{
  [ImportMany( typeof( IPagePlugin ) )]
  public List<Lazy<IPagePlugin, IPagePluginMetadata>> Items
  {
    get;
    set;
  }
}
 
PagePluginCollection oPagePluginCollection = new PagePluginCollection( );
 
AggregateCatalog oAggregateCatalog = new AggregateCatalog( );
 
Uri oUri = new Uri( Assembly.GetExecutingAssembly( ).CodeBase );
 
string szPath = Path.Combine( Path.GetDirectoryName( oUri.LocalPath ), "Plugin" );
 
if ( Directory.Exists( szPath ) )
{
  oAggregateCatalog.Catalogs.Add( new DirectoryCatalog( szPath ) );
}
 
CompositionContainer oCompositionContainer = new CompositionContainer( oAggregateCatalog );
 
oCompositionContainer.ComposeParts( oPagePluginCollection );

AggregateCatalog (from MEF) loads the assemblies from the folder specified. CompositionContainer used to holds all the composable parts found in those assemblies, including its dependencies. In the last line the ComposeParts method loads the composable parts into the PagePluginCollection list. I'm using here Lazy to defer the initialization of the actual object, and that for save resources in case no plugins will be used (without the Lazy part, the last step would create real objects form the loaded composable parts).

Find the Plugin

It's really easy. I used LINQ here, but any way to find a item in a list will do it. One thing we can learn is how we can access those metadata bit we added to our plugin page (MEF does it a-piece-of-cake).

C#
oPagePluginCollection.Items.Find(
  ( oPlugin ) =>
  {
    return ( ( oPlugin.Metadata.ExtensionPoint == "Login" ) && ( oPlugin.Metadata.PluginName == "LoginExtension" ) );
  }
); 

At this point I have the plugin page in our hand and can get into the rendering, but just before that an other interesting point. In the code sample I used some literal values to find the plugin page but you may see the opportunity to use variables that came from the extensible page (ExtensionPoint) and from some configuration (PluginName). With variables it is possible that you have numerous plugin pages for the same extension page and you may choose to load a different one every time.

VirtualPathProvider

To make it simple I will start this part from the outside, that how to use a virtual path provider of your own. The code below goes into the Global.asax.cs...

C#
public class Global : HttpApplication
{
  protected void Application_Start ( object sender, EventArgs e )
  {
    HostingEnvironment.RegisterVirtualPathProvider( new EmbeddedPageProvider( ) );
    ExtensionUtils.InitPlugins( Application );
  }
} 

RegisterVirtualPathProvider adds my home-made provider to the ASP.NET compilation system. It lets me to provide a different - virtual - file, instead the one the default ASP.NET provider presents (The InitPlugins method - that not presented here - used to load all the plugins into the application. Its code almost identical to the one presented in Load Plugin Pages section).

The provider itself is the most complicated code of the solution...

C#
public class ExtensionVirtualFile : VirtualFile
{
  PagePluginItem _Plugin;
  
  public ExtensionVirtualFile ( string VirtualPath, PagePluginItem Plugin )
    : base( VirtualPath )
  {
    _Plugin = Plugin;
  }
  
  public override Stream Open ( )
  {
    return ( _Plugin.Value.Assembly.GetManifestResourceStream( _Plugin.Value.ResourceID ) );
  }
}
 
public class EmbeddedPageProvider : VirtualPathProvider
{
  public override bool FileExists ( string VirtualPath )
  {
    if ( HttpContext.Current.CurrentHandler is ExtensiblePage )
    {
      return ( true );
    }
    
    return ( base.FileExists( VirtualPath ) );
  }
  
  public override VirtualFile GetFile ( string VirtualPath )
  {
    if ( HttpContext.Current.CurrentHandler is ExtensiblePage )
    {
      string szExtensionPoint = 
        ( ( ExtensiblePage )HttpContext.Current.CurrentHandler ).ExtensionPoint;
      string szName = ExtensionUtils.GetConfigValue( 
        HttpContext.Current.Application, szExtensionPoint );
      
      PagePluginItem oActivePlugin = ExtensionUtils.GetActivePlugin( 
        HttpContext.Current.Application, szExtensionPoint, szName );
      
      return ( new ExtensionVirtualFile( VirtualPath, oActivePlugin ) );
    }
    
    return ( base.GetFile( VirtualPath ) );
  }
  
  public override CacheDependency GetCacheDependency ( 
    string VirtualPath, IEnumerable VirtualPathDependencies, DateTime UTCStart )
  {
    if ( HttpContext.Current.CurrentHandler is ExtensiblePage )
    {
      return ( null );
    }
    
    return ( base.GetCacheDependency( VirtualPath, VirtualPathDependencies, UTCStart ) );
  }
}

FileExist checks if the requested path is one of mine to handle, if it returns true the ASP.NET rendering will call the GetFile method to retrieve an VirtualFile that in his turn will supply a Stream with the files content.

Render the Plugin Page

Just before the ASP.NET rendering engine start to handle the page I have to redirect it to the replacement page, if any. For that I'm using the Application_PreRequestHandlerExecute method in Global.asax.cs...

C#
protected void Application_PreRequestHandlerExecute ( object sender, EventArgs e )
{
  if ( HttpContext.Current.CurrentHandler is ExtensiblePage )
  {
    ExtensiblePage oExtensiblePage = ( ExtensiblePage )HttpContext.Current.CurrentHandler;
    string szExtensionPoint = oExtensiblePage.ExtensionPoint;
    PagePluginCollection oPlugins = ( PagePluginCollection )Application[ _PluginRoot ];
    string szConfigValue = GetConfigValue( Application, szExtensionPoint );
 
    if ( oPlugins != null )
    {
      PagePluginItem oActivePlugin = oPlugins.Items.Find(
        ( oPlugin ) =>
        {
          return ( ( oPlugin.Metadata.ExtensionPoint == szExtensionPoint ) && 
                         ( oPlugin.Metadata.PluginName == szConfigValue ) );
        }
      );
      
      if ( oActivePlugin != null )
      {
        Server.Transfer( string.Format( "{0}/{1}/{2}/{3}", _PluginPageRoot, 
          oActivePlugin.Metadata.ExtensionPoint, 
          oActivePlugin.Metadata.PluginName, oActivePlugin.Value.ResourceID ), true );
      }
    }
  }
} 

In a very simple way I do Server.Transfer to some made-up path, that identifies the plugin page. I found two advantages to this solution:

  1. No URL change on client side
  2. The request's data preserved and available to the new page

That summarizes the code part of the idea I had, but there are some more technical issues to take care...

The Plugin is Embedded

As one of the pre-request was to do single file deployment, the markup have to be embedded inside the DLL, together with the code. To do that you must select page properties in Visual Studio and change Build Action to Embedded Resource.

An other part of this embedded story is in the markup. When Visual Studio generates your basic markup it starts every page wit a line like this:

ASP.NET
<%@ Page Language="C#" CodeBehind="Default.aspx.cs" Inherits="Default,Plugin" %> 

When your page embedded the Inherits property - as is - will not find the code behind, so you must change it by adding namespace, like this:

ASP.NET
<%@ Page Language="C#" CodeBehind="Default.aspx.cs" Inherits="Plugin.Default,Plugin" %> 

Where My Plugin Is

By default all binary files for a site sit in the \bin folder, in this solution I decided to put all the plugin files in a sub-folder - for better maintain. However ASP.NET will not find those assemblies without a minor modification of the web.config (the binary folder not a fixed values of IIS/ASP.NET but a configuration option).

XML
<runtime>
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
    <probing privatePath="bin;bin\Plugins" />
  </assemblyBinding>
</runtime>  

This change will tell ASP.NET to look for assemblies also in the sub-folder Plugins.

AJAX Problem 

The action attribute of the form element used by AJAX as a target for posting-back in partial rendering. In our case the URL displayed to the user isn't changing even a plugin activated, but the real content of the page does reflect those changes. One of those changes is the value of the action attributes that will be the path used by Server.Transfer. The problem is that the property changes on post back because of the virtual nature of the page. This change will cause an error (404) on the second AJAX post-back. To solve I added OnLoad override to the plugin page... 

C#
protected override void OnLoad ( EventArgs e )
{
  base.OnLoad( e );
  
  if ( !IsPostBack )
  {
    Form.Action = ResolveClientUrl( Request.AppRelativeCurrentExecutionFilePath );
  }
} 

Demo

The demo solution attached show a case to replace the original login page. In the demo I added all the bits and rounded up with some configuration handling...

Download Demo 

Last Words

At this point, we (the company), not yet have the new customer, but I already learned a lot of useful and powerful things. If you do so please take a moment and let me know how you used the ideas I got... 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Israel Israel
Born in Hungary, got my first computer at age 12 (C64 with tape and joystick). Also got a book with it about 6502 assembly, that on its back has a motto, said 'Try yourself!'. I believe this is my beginning...

Started to learn - formally - in connection to mathematics an physics, by writing basic and assembly programs demoing theorems and experiments.

After moving to Israel learned two years in college and got a software engineering degree, I still have somewhere...

Since 1997 I do development for living. I used 286 assembly, COBOL, C/C++, Magic, Pascal, Visual Basic, C#, JavaScript, HTML, CSS, PHP, ASP, ASP.NET, C# and some more buzzes.

Since 2005 I have to find spare time after kids go bed, which means can't sleep to much, but much happier this way...

Free tools I've created for you...



Comments and Discussions

 
GeneralMy vote of 5 Pin
Prasad Khandekar10-Nov-13 23:40
professionalPrasad Khandekar10-Nov-13 23:40 
QuestionGood Article, theres a problem though between the article and the sample Pin
PinguimJ25-Oct-13 1:38
PinguimJ25-Oct-13 1:38 
AnswerRe: Good Article, theres a problem though between the article and the sample Pin
Kornfeld Eliyahu Peter26-Oct-13 9:18
professionalKornfeld Eliyahu Peter26-Oct-13 9:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.