Introduction  

I'd like to share the way I built an ASP.NET MVC plugin framework, it mainly contains the below features,

  1. Each plugin can be deployed in a seperate folder;  No need to copy plugin assembly to bin folder, the plugin files keep same structure as an ordinary website;
  2. Dynamically install/uninstall plugin after website gets running;
  3. Plugins share same master/layout;

Background 

There are many discussions about building ASP.NET MVC plugin framework, I saw a good article here: http://www.codeproject.com/Articles/358360/NET-4-0-ASP-NET-MVC-3-plug-in-architecture-with-e, but it requires all views be embedded into the assembly, which is a little bit unacceptable to developers in real life.

Another way is, when installing a plugin, copy its views to Views folder, this seems a solution but it's very diffcult to maintain individual plugin, especially when the size of plugin grows large.

The backend plugin framework is based on OSGi.NET, technically, you can replace it with anyother  frameworks like MEF, Sharpdevelop with some wrapping. 

Using the code 

Download source code for ASP.NET MVC 3 and ASP.NET MVC4 plugin framework at here

// Load plugins in HttpApplication.
protected void Application_Start()
{
    //Start OSGi
    var bootstapper = new Bootstrapper();
    bootstapper.StartBundleRuntime();

    //Register Razor view engine for bundle.
    ViewEngines.Engines.Add(new BundleRuntimeViewEngine(new BundleRazorViewEngineFactory()));
    //Register WebForm view engine.
    ViewEngines.Engines.Add(new BundleRuntimeViewEngine(new BundleWebFormViewEngineFactory()));

    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
    MonitorExtension();
}

The code above is pretty neat and clear, but I still have some brief comments here.

  1. The first two lines is to start OSGi.NET, which will load plugins and resolve their dependency, after this, all plugins should be active or ready to use;
  2. The next two lines is to register view engines for plugins, this is crucial of the plugin framework. In this framework, each plugin and its views, assemblies and all private items can be deployed in a separate folder, by default, ASP.NET can't find the views when user try to access them, so I need to customize a new one to help ASP.NET can locate the resource from correct folder.
  3. The last one MonitorExtension is used to hook plugin changing, like install new plugin or uninstall, in this case we should update the master pages correspondingly.

Points of Interest 

How does ASP.NET locates plugin assembly since its assembly doesn't copy to bin?

This is pretty tricky here, ASP.NET resolves assembly from class BuildManager, so you should register plugin assembly into there after the plugin is active, or remove it when deactivated.   

How to display views in plugin to master page?

Each plugin has a manifest file, which describes all resources in it. Take the BlogPlugin in the source code for example, its manifest is this,

<?xml version="1.0" encoding="utf-8"?>
<Bundle xmlns="urn:uiosp-bundle-manifest-2.0" Name="BlogPlugin" 
         SymbolicName="BlogPlugin" Version="1.0.0.0" InitializedState="Active">
  <Activator Type="BlogPlugin.Activator" Policy="Immediate" />
  <Runtime>
    <Assembly Path="bin\BlogPlugin.dll" Share="false" />
  </Runtime>

  <Extension Point="MainMenu">
      <Item url="/BlogPlugin/Blog/Index" text="Blog" order="4"/>
      <Item url="/BlogPlugin/Support/Index" text="Support" order="2"/>
  </Extension>
</Bundle>

The extension node indicates this plugin will add Blog and Support link to Main menu in layout page. Let's go back to MoniteExtension, it's used to monitor any changes of plugin extension, in this case, it will load extension info into ApplicationViewModel, then render layout page/Master page dynamically. The MoniterExtension method is as follows:

private void MonitorExtension()
{
    ViewModel = new ApplicationViewModel();

    //Register pages in Shell project.
    ViewModel.MainMenuItems.Add(new MenuItem
    {
        Text = "Home",
        URL = "/"
    });

    BundleRuntime.Instance.AddService<ApplicationViewModel>(ViewModel);
    _extensionHooker = new ExtensionHooker(BundleRuntime.Instance.GetFirstOrDefaultService<IExtensionManager>());
    _extensionHooker.HookExtension("MainMenu", new MainMenuExtensionHandler(ViewModel));
    _extensionHooker.HookExtension("SidebarMenu", new SidebarExtensionHandler(ViewModel));
}

And below is how we render main menu in layout page,

<div class="header">
    <div class="header_resize">
        <div class="logo">
            <h1><a href="http://www.codeproject.com/">OSGi.NET</a> 
                  <small>This page is built by <b>Razor Engine</b></small></h1>
        </div>
        <div class="menu_nav">
            <ul>
                @{
                    var viewModel = UIShell.OSGi.BundleRuntime.Instance.GetFirstOrDefaultService<ApplicationViewModel>();
                    if (viewModel != null)
                    {
                        foreach (var mainMenuItem in viewModel.MainMenuItems.OrderBy(item => item.Order))
                        {
                            <li itemid="@mainMenuItem.Order"><a href="@mainMenuItem.URL">@mainMenuItem.Text</a> </li>
                        }
                    }
                }
            </ul>
        </div>
        <div class="clr"></div>
    </div>
</div>

And here is the running mode,

Here is how it happens,

How to support dynamically plugin installation?

By default, after ASP.NET application get started, you are not allowed to register assembly to BuildManager anymore,  I did a risky try here, which is get the assembly container from BuildManager by reflection, then register newly installed plugin assembly.

History 

None.

Last edited May 20, 2014 at 4:46 AM by shalahu, version 6

Comments

No comments yet.