Introduction
In this article, I'd like to share my ASP.NET MVC plugin framework, it mainly contains the below features,
- Each plugin can be deployed in a separate folder; No need to copy plugin assembly to bin folder, the plugin files keep same structure as an ordinary website;
- Dynamically install/uninstall plugin after website gets running;
- Plugins share same master/layout;
- Plugins can have same controller name;
- Support both web form engine and Razor engine;
Background
There are many discussions about building an ASP.NET MVC plugin framework, but most of them use below tricks or similar,
- All views be embedded into the assembly, or
- Copy plugin assemblies into bin, or
- Use private path to indicate where the plugin's assembly is, or
- After installing a plugin, copy its views to Views folder,
All of them seems attractive but it's very difficult to maintain individual plugin, especially when the size of plugin grows large.
In this article, you will see creating a plugin is almost exactly same as create a regular ASP.NET MVC web application, it only need to create a plugin manifest file for each plugin.
The ASP.NET MVC plugins is actually a extension based on another plugin framework OSGi.NET, technically, you can replace it with any other frameworks like MEF, Sharp-develop with some wrapping.
Using the code to Create a Plugin
Now let go through how to create a new plugin from scratch base on the plugin framework. I will create a media management plugin, which can display the most popular TV shows. The key steps are shown below,
- Download the latest ASP.NET MVC Plugin Framework source code (you can choose MVC3 or MVC4 depends on what Visual studio version you have) and extract it to a separate folder, then open MvcOSGi.sln with. The solution skeleton is shown below:
The projects under “Core” are the plugin framework, Plugins folder is the plugin container, the start up project “MvcOSGi.Shell” is a standard ASP.NET MVC web application, the only thing it does is to start OSGi.Net framework at Application_Start()
, the code is show below.
protected void Application_Start()
{
var bootstapper = new Bootstrapper();
bootstapper.StartBundleRuntime();
ViewEngines.Engines.Add(new BundleRuntimeViewEngine(new BundleRazorViewEngineFactory()));
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.
- 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;
- 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.
- 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.
- Right click Plugins folder, Add-New Project-ASP.NET MVC 4 Web Application, input the project name as MediaPlugin, and click OK to finish plugin project.
- Add packages\iOpenworks\UIShell.OSGi.dll to MediaPlugin reference, it is the backend plugin framework library.
- Create a controller named PopularTVShowController and it’s view;
- Add a XML file named Manifest.xml to MediaPlugin project, its content is shown below,
="1.0" ="utf-8"
<Bundle xmlns="urn:uiosp-bundle-manifest-2.0" Name="MediaPlugin"
SymbolicName="MediaPlugin" Version="1.0.0.0" InitializedState="Active">
<Runtime>
<Assembly Path="bin\MediaPlugin.dll" Share="false" />
</Runtime>
<Extension Point="SidebarMenu">
<Item url="/PopularTVShow/Index?plugin=MediaPlugin"
text="Popular Movie" order="0"/>
<Item url="https://osgi.codeplex.com/" text="OSGi.NET" order="0"/>
<Item url="#" text="Style" order="0"/>
<Item url="#" text="Blog" order="0"/>
<Item url="#" text="Archives" order="0"/>
</Extension>
</Bundle>
It is the plugin manifest file; it specifies the plugin assemblies and pages to display to end user. I will talk about it later.
Lastly, rebuild the whole solution and press F5 to run.
You will see the links of your plugin pages list in the home page, here is how it happens,
I will explain how to display views in plugin to master page later. Let's click the "Popular Movie" link to check out the page in plugin,
You will find the plugin specify the plugin name via query string. This is how the framework handle the isue that same controller name in multiple plugins.
Points of Interest
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,
="1.0" ="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="/Blog/Index?plugin=BlogPlugin"
text="Blog" order="4"/>
<Item url="/Support/Index?plugin=BlogPlugin"
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();
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 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 support dynamically plugin installation?
I am using a free plugin framework OSGi.NET to manage the plugins, which natively supports dynamicly install and uninstall plugins. Please refer to OSGi.NET Modulization Framework .
Which shows how to install or stop plugin in a remote console app. I wrap the MVC plugin framework base on it, When new plugin is installed, I can receive a notification then I just need to add assemblies in the plugin to BuildManager.
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.
Why don't need to restart w3wp process during dynamical installation?
By default w3wp will load all assemblies in Bin folder and lock them, there is no way to release the lock without reset. But you don't need to. In OSGi.net, when plugin is removed, all resources in it are released, so the assembly is invisible to other plugins like it never exist. The key point is plugin assembly is placed in it's private folder, not bin folder, so w3wp does not load it, but osgi.net does, in this way, removing the plugin assembly won't recycle w3wp.
History
- Elaborate dynamic installation/uninstallation, and why w3wp doesn't recycle.