Griffin.MvcContrib – The plugin system

Introduction

Griffin.MvcContrib is a contribution project for ASP.NET MVC3 which contains different features like extendable HTML Helpers (you can modify the HTML that the existing helpers generate), easy localization support and a quite fresh support for plugin development. The purpose of this article is to provide a step-by-step instruction on how you can develop plugins using Griffin.MvcContrib.

Using the code

The plugin support are based on the Area support which MVC3 got. If you haven’t used areas before, I suggest that you start by reading the following article.

Most basic approach.

This first sample is not really a plugin system but only showing how you can move your code to class libraries. The example will only contain one class library to avoid confusion.
Start by creating a new ASP.NET MVC3 project (of any kind) and a class library project. For convenience we would like to have support for Razor and all MVC3 wizards in our class library. It helps us add some basic files which are required to get everything working.
To get that support we need to modify the project file for the class library. Here is how:

1. Right-click on the class library project and choose unload project:

2. Right-click on the project file and select Edit project file

3. Add the following XML element on a new line below <ProjectGuid>

<ProjectTypeGuids>{E53F8FEA-EAE0-44A6-8774-FFD645390401};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>  

4. Save and close the project file.

5. Right-click and reload the project.

6. Add reference to “System.Web” and “System.Web.Mvc” (project settings)

You have now told Visual Studio that it should active the tooling and Razor 3 views. We can now add an area by just right-clicking on the project and select “Add Area”.

Do so and create an area with a name of your choosing. Create a controller (with an Index action) and a view for the index action. You now got your first external DLL for the MVC project.

Add a reference to the class library from the MVC3 project:

Congratulations. You now got your first “plugin” based solution. Hit F5 to run the project. I named my area “Ohh” and my controller “My” so I surf to “http://theHostNameAndPort/ohh/my” to visit my plugin controller.

Uh oh. We can visit the page alright. But the view cannot be found. It can be solved thanks to Griffin.MvcContrib. Let’s install the project in the MVC3 project:

Then open up global.asax and create a new method which maps the views:

protected void Application_Start()
{ 
    AreaRegistration.RegisterAllAreas();
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
    RegisterViews();
}
protected void RegisterViews()
{
    var embeddedProvider = new EmbeddedViewFileProvider(HostingEnvironment.MapPath("~/"), new ExternalViewFixer());
    embeddedProvider.Add(new NamespaceMapping(typeof(Lib.Areas.Some.Controllers.MyController).Assembly, "BasicPlugins.Lib"));
    GriffinVirtualPathProvider.Current.Add(embeddedProvider);
    HostingEnvironment.RegisterVirtualPathProvider(GriffinVirtualPathProvider.Current);
} 

The first line creates a new file provider which loads files from embedded resources. We tell it which path it should try to provide files for. The next line simply adds our class library assembly and tell which root namespace it has. To make this work, we also have to change our view to an embedded resource:

Everything should be set now. Hit F5 and visit the area controller again. You should see the view now.

You can also try to have a break point in the class library. Debugging should work just like it used to. There is still one “problem”. We can’t modify the views at runtime, we need to recompile the project each time we adjust a view. Fortunually for us, Griffin.MvcContrib can solve that too. Open the RegisterViews method in global.asax again. Let’s add a disk file provider. It should be added before the embedded provider since the first found file will be used.

var diskLocator = new DiskFileLocator();
diskLocator.Add("~/", Path.GetFullPath(Server.MapPath("~/") + @"..BasicPlugins.Lib"));
var viewProvider = new ViewFileProvider(diskLocator);
GriffinVirtualPathProvider.Current.Add(viewProvider);  

You can now edit the views at runtime.

Building a plugin system

The last part showed you how you can put Controllers, Models and Views in class libraries. It’s quite useful as long as you don’t want to develop features in a more loosely coupled and dynamic way. This section will suggest a structure which you can use when building plugins.

The following part expects you to have used inversion of control containers in the past. The container is used to provide the application extension points to all plugins. I prefer using a container which has a module system so that each plugin can take care of all it’s registrations itself. I’m using Autofac in this article and it’s Module extension.

Let’s place all code which is shared between the plugins and the MVC3 project in a seperate class library. You could also follow follow the Separated interface pattern and only define all interfaces in that project. Thus removing all direct dependencies between the plugins and the main application.

The last thing to remember is that the default route configuration doesn’t work very well if you have controllers with the same name in different areas. To overcome that you have to manually change all route mappings to include the namespace. Also remove the “_default” from the route name.

Modified registration:

public override void RegisterArea(AreaRegistrationContext context)
{
	context.MapRoute(
		"Messaging_default",
		"Messaging/{controller}/{action}/{id}",
		new { action = "Index", id = UrlParameter.Optional },
		new[] { GetType().Namespace + ".Controllers" }
	);
}

Create a new class library named something like “YourApp.PluginBase” and add our basic extension points:

public interface IRouteRegistrar
{
	void Register(RouteTable routes);
}

Used to register any custom routes.

public interface IMenuRegistrar
{
	void Register(IMenuWithChildren mainMenu);
}

Allows the plugins to register themselves in the main application menu.

We’ll just use those two extensions in this example. Feel free to add as many extensions as you like in your own project ;)

The structure

We’ll need to have some structure for the plugins so that the can easily be managed, both during development and in production. All plugins will therefore be placed in a sub directory called “Plugins”. Something like:

ProjectName\Plugins
ProjectName\Plugins\PluginName
ProjectName\Plugins\PluginName\Plugin.PluginName
ProjectName\Plugins\PluginName\Plugin.PluginName.Tests
ProjectName\ProjectName.Mvc3

Add a new solution folder by right-clicking on the solution folder in the Solution Explorer. Do note that solution folders doesn’t exist on the disk and you’ll therefore have to manually append the folder to the location text box each time you add a new project in it.

Right-click on the Plugins folder in Solution Explorer and add a new project named “PluginName”. Don’t forget to append PluginsPlugin.Name to the location text box.

Since we this time don’t want to have any references to the plugins from the main application we have to manually copy them to the main application plugin folder. We use a post build event for that. Don’t forget to copy in all dependencies that your project has, since nothing handles that for you (the price of having no direct references from the MVC3 project to the plugins).

Complete structure:

Menu items

Keep in mind that you can not unload plugins and that they are available for all users. The easiest way to come around that is to display menu items only if the user has a role, which means that you should create one (or more) role(s) per plugin (if the plugin should not be available for all users). Griffin.MvcContrib contains a basic menu implementation which allows you to control if menu items are visible or not.

Sample code:

var item = new RouteMenuItem("mnuMyPluginIndex", "List messages", new { 
        controller = "Home", 
        action = "Index", 
        area = "Messages"});
item.Role = "MessageViewer";
menuRegistrar.Add(item);

You can later use menuItem.IsVisible when you generate the menu to determine if the item should be included or not.

Hello container

For this exercise we’ll use Autofac as the container. It contains a nifty module system which aids us in keeping the projects loosely coupled. Each plugin need to create a class which inherits from Autofac.Module and use that class to register all services which the plugin provides. Thanks to that we’ll just have to scan all plugin assemblies after any container modules and invoke them with our container.

public class ContainerModule : Module
{
	protected override void Load(ContainerBuilder builder)
	{
		builder.RegisterType<MenuRegistrar>().AsImplementedInterfaces().SingleInstance();
		builder.RegisterType<HelloService>().AsImplementedInterfaces().InstancePerLifetimeScope();
		base.Load(builder);
	}
}

The plugins are loaded from the main application using the following snippet:

var moduleType = typeof (IModule);
var modules = plugin.GetTypes().Where(moduleType.IsAssignableFrom);
foreach (var module in modules)
{
	var mod = (IModule) Activator.CreateInstance(module);
	builder.RegisterModule(mod);
}

Views during development

Since we want to be able to modify views at runtime during development we have to tell ASP.NET MVC3 where it can find our plugin views. We do this by using GriffinVirtualPathProvider and a custom file locator which looks like this:

public class PluginFileLocator : IViewFileLocator
{
    public PluginFileLocator()
    {
        _basePath = Path.GetFullPath(HostingEnvironment.MapPath("~") + @"..Plugins");
    }

    public string GetFullPath(string uri)
    {
        var fixedUri = uri;
        if (fixedUri.StartsWith("~"))
            fixedUri = VirtualPathUtility.ToAbsolute(uri);
        if (!fixedUri.StartsWith("/Areas", StringComparison.OrdinalIgnoreCase))
            return null;

        // extract area name:
        var pos = fixedUri.IndexOf('/', 7);
        if (pos == -1)
            return null;
        var areaName = fixedUri.Substring(7, pos - 7);

        var path = string.Format("{0}{1}\Plugin.{1}{2}", _basePath, areaName, fixedUri.Replace('/', '\'));
        return File.Exists(path) ? path : null;
    }
}

It simply takes the requested uri and converts it using the naming standard I described above. Everything else is taken care of by Griffin.MvcContrib.

And interesting part is that the provider is only loaded during development thanks to nifty helper class in Griffin.MvcContrib:

if (VisualStudioHelper.IsInVisualStudio)
    GriffinVirtualPathProvider.Current.Add(_diskFileProvider);

Loading the plugins

Ok. We’ve created some plugins (and their dependencies) which are copied to the bin folder with a post build event. We’ll therefore have to load them in some way. To do this we create a new class looking like this:

public class PluginService
{
    private static PluginFinder _finder;
    private readonly DiskFileLocator _diskFileLocator = new DiskFileLocator();
    private readonly EmbeddedViewFileProvider _embededProvider =
        new EmbeddedViewFileProvider(new ExternalViewFixer());
    private readonly PluginFileLocator _fileLocator = new PluginFileLocator();
    private readonly ViewFileProvider _diskFileProvider;

    public PluginService()
    {
        _diskFileProvider = new ViewFileProvider(_fileLocator, new ExternalViewFixer());

        if (VisualStudioHelper.IsInVisualStudio)
            GriffinVirtualPathProvider.Current.Add(_diskFileProvider);

        GriffinVirtualPathProvider.Current.Add(_embededProvider);
    }


    public static void PreScan()
    {
        _finder = new PluginFinder("~/bin/");
        _finder.Find();
    }

    public void Startup(ContainerBuilder builder)
    {
        foreach (var assembly in _finder.Assemblies)
        {
            // Views handling
            _embededProvider.Add(new NamespaceMapping(assembly, Path.GetFileNameWithoutExtension(assembly.Location)));
            _diskFileLocator.Add("~/",
                                 Path.GetFullPath(HostingEnvironment.MapPath("~/") + @"...." +
                                                  Path.GetFileNameWithoutExtension(assembly.Location)));

            //Autofac integration
            builder.RegisterControllers(assembly);
            var moduleType = typeof (IModule);
            var modules = assembly.GetTypes().Where(moduleType.IsAssignableFrom);
            foreach (var module in modules)
            {
                var mod = (IModule) Activator.CreateInstance(module);
                builder.RegisterModule(mod);
            }
        }
    }

    // invoke extension points
    public void Integrate(IContainer container)
    {
        foreach (var registrar in container.Resolve<IEnumerable<IMenuRegistrar>>())
        {
            registrar.Register(MainMenu.Current);
        }

        foreach (var registrar in container.Resolve<IEnumerable<IRouteRegistrar>>())
        {
            registrar.Register(RouteTable.Routes);
        }
    }
}

The plugins are now loaded and the extension points have been passed to them.

Final touch

The last thing to change is global.asax:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

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

    _pluginServicee = new PluginService();
    RegisterContainer();
    HostingEnvironment.RegisterVirtualPathProvider(GriffinVirtualPathProvider.Current);
    _pluginServicee.Integrate(_container);
}

private void RegisterContainer()
{
    var builder = new ContainerBuilder();
    builder.RegisterControllers(Assembly.GetExecutingAssembly());
    _pluginServicee.Startup(builder);
    _container = builder.Build();
    DependencyResolver.SetResolver(new AutofacDependencyResolver(_container));
}

Code

The code for the samples are located in github and so is Griffin.MvcContrib. Griffin.MvcContrib can also be install from nuget.


  • John

    Is it possible to put javascript/resource files in a ‘plugin’ lib?

    • http://www.gauffin.org jgauffin

      yes

  • will eizlini

    when I follow your instructions I get errors in the class library project for any view that is not static text and makes reference to a model. in fact the @Model.var syntax is not recognized at all, and we are told that a buildprovider is needed to be declared for cshtml files. did you have this same problem and how did you resolve it…any ideas ?
    thanks in advance

    • http://www.gauffin.org jgauffin

      Depends on if you get the error at runtime or in visual studio (when opening the view). If it’s during runtime you’ll probably forgot to use the ExternalViewFixer. If it’s in Visual Studio you have either not specified the ProjectTypeGuids in the .csproj file for the library, or not used the “Add Area” wizard to get the area (it adds a view.config which is required by Visual Studio & Razor)

  • Michael

    Is this something that can be used in the scenario when you want to have one codebase serving multiple subdomain sites (codebase.site1.com, codebase.site2.com, etc.)? Each of the sites has different “forms” (or services), but I want them to share common elements (profile, account, admin).

    I guess it would sort of be like multi-tenancy? Is this possible?
    Thanks.

    • http://www.gauffin.org jgauffin

      No. I’ve played around with the virtual path provider before to provide different views for different contexts. I do not recommend that.

      I usually use RavenDb for my projects. It has great support for multi-tenancy.

  • Michael

    Don’t know if I am doing something wrong, but I downloaded your code for the samples. When I view the site, I see the “Messages” tab, click it and it takes me to the Messages Index page. If I hit “Home” in the Messages area, it takes me back to the Main Home page, which is expected. However, if I keep clicking the tabs eventually when I am in the Messages area it will not go back Home if I click the Home tab.

    Any thoughts?

    • Michael

      I think I resolved this by updating the ProjectTypeGuids in the .csproj file to be {E53F8FEA-EAE0-44A6-8774-FFD645390401};{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} (notice the extra set in the middle from what you have in the post and in code sample). VS2010 then asked me if I wanted to convert/update the project, which I did. Now it appears to work.

      • http://www.gauffin.org jgauffin

        No. Don’t do that. You just made the class library a complete web site. you’ll get two dev webservers each time you run your project. Change back.

      • will eizlini

        i did the same thing so that I could edit views with syntax checking and contextual help and code completion within the views. Is there another way to do this…?
        I solved the webserver problem by pointing it to the localhost of the test project localhost address, and so it does not spawn a new server

      • mark

        I have changed the value of ProjectTypeGuids as you suggested, but it seems still not to work as expected. Do you have any other ways to resolve this problem.

        • jeffrey

          Define area as empty string in the ActionLink for the home/about tabs. At some point it seems to stay in the Messages area when you don’t explicitly define the ‘empty’ area for the home button actionlinks.

          This solved it for me:
          @Html.ActionLink(“Home”, “Index”, “Home”, new { area = “” }, null)

    • http://www.gauffin.org jgauffin

      I don’t remember how I built the sample. but I think that both the area and the main project has a “Home” controller. And that’s where the mixup is. simply change so that the “Home” action link uses the default route.

  • Guilherme

    Thank you very much.. That’s a very nice tool !!

  • Guilherme

    Hello,

    Could you please explain me how you deal with Html Helpers, specifically with your own extension methods.. I could use them adding the @using statement inside views, but this is something I’d like to avoid.. I also solved that problem creating my own ExternalViewFixer..

    Do you see any other alternative ?

    Thanks in advance..

  • http://www.fibit.cz Martin

    Hello, It is possible to do some installator for plugins ? For example uses Nuget ?
    Thanks ! Nice Post !

  • Sean

    Thanks so much for this awesome library. I have everything up and running following your tutorial, but would like to expand upon it. How would I handle having an embedded layout in my plugin? The path to the [Area]Shared_layout.cshtml is not resolved to an embedded resource – ASP.NET throws an error saying the layout cannot be found. Any ideas?

    • Sean

      Nevermind – I got it!

      I downloaded the source and started digging through it and realized the PluginFileLocator you show the code for above in your post is actually already built in to the library – no need to roll my own. Once I used it & matched up my folder structure: editing view files and using nested layouts worked perfectly.

      Thanks again!

  • jeffrey

    Hi,

    Good stuff! Exactly what I was looking for! I’ve been looking at the PluginSystemDemo project for MVC3, updated the MvcContrib library to the latest version and noticed the EmbeddedFileProvider, which is used for files other than views.

    How should I use that? What I’m trying to do is use seperate CSS files for my plugin. So I have a Content/CSS/Plugin.css file inside the plugin class library, but how can I use it? I’ve added it to the GriffinVirtualPathProvider inside PluginService.cs and I’m trying to use Url.Content(“~/Content/CSS/Plugin.css”), which doesn’t work. But If I use Url.Content(“~/Areas/Messaging/Content/CSS/Plugin.css”), it gets loaded, but the ‘usings’ are written to the CSS file, as if it’s a view.

    So my question is, what’s the best way to use seperate css, image and/or js files for the plugin projects?

    Thanks in advance!

    • jeffrey

      Ok figured out how to use the EmbeddedFileProvider. I did it right the first time, but the ExternalViewFixer for the ViewFileProvider seems to break it.

      I added this to PluginService.cs:

      private readonly EmbeddedFileProvider _embeddedContentProvider = new EmbeddedFileProvider(VirtualPathUtility.ToAbsolute("~/"));
      GriffinVirtualPathProvider.Current.Add(_embeddedContentProvider);
      

      It didn’t seem to work, until I commented out this:

      _diskFileProvider = new ViewFileProvider(_fileLocator, new ExternalViewFixer());
      
      if (VisualStudioHelper.IsInVisualStudio)
          GriffinVirtualPathProvider.Current.Add(_diskFileProvider);
      

      That caused the css and js files to be modified with the ‘inherits’ by the ExternalViewFixer.

      Am I missing something or doing something wrong, because I like the fact I can edit views and see the changes without building the project first, but I need the ExternalViewFixer for that.

      Thanks!

      • http://www.gauffin.org jgauffin

        use one EmbeddedFileProvider for the content and a EmbeddedViewFileProvider for the views. i.e. do not use the same since Views need to be processed a bit to work from other assemblies.

        • jeffrey

          Thanks for the reply.

          That’s exactly what I’m doing. I’m using both and both are added to the GriffinVirtualPathProvider.

          This is what I did:
          I’ve used the example project (PluginSystemDemo) for testing. I’ve updated the griffin.mvccontrib package to the latest version and added the EmbeddedFileProvider (so I did not replace EmbeddedViewFileProvider). I added a Content/CSS folder to the Plugin.Messaging project Areas folder, created a css file, configured it to be an embedded resource and link to ~/Areas/Messaging/Content/CSS/Messaging.css.

          If I run the project and check the CSS file, I see all the inherits in the file, as if it’s processed as a view by the ExternalViewFixer. Same happens to JS files.

    • http://www.gauffin.org jgauffin

      I’ll update the example as soon as I have some time over.

      • kevin

        any updates on this topic? I’m getting the same troubles when trying to embedded js and css files in my plugin.

        • http://www.gauffin.org jgauffin

          Create an issue at github

        • jeffrey

          The problem is that the ViewFileProvider (which is used to be able to refresh page without rebuilding) doesn’t have an AllowedFileExtensions array like the EmbeddedFile/EmbeddedVIEWFileProvider have. These providers only allow certain extensions and behave different.

          So I’ve build in an AllowedFileExtensions array in the ViewFileProvider class and only let view files get reparsed by the CorrectView function. If the file is something else, like js, css or anything, it does not get reparsed and all is well.

          Maybe I’ll rename the class to DiskFileProvider, since it handles view files and content files like js/css and images in a different way and it’s no longer a ViewFileProvider.

          Anyway, here is the code I added to ViewFileProvider.cs:

          ///
          /// Allowed files that may be fixed by the _viewFixer
          ///
          public string[] AllowedFixFileExtensions = new[] { “cshtml”, “ascx”, “aspx” };

          ///
          /// determines if the found embedded file may be fixed
          ///
          ///
          ///
          /// Default implementation uses to determine which files to fix.
          protected virtual bool IsFileFixAllowed(string fileName)
          {
          if (fileName == null) throw new ArgumentNullException(“fileName”);

          var extension = fileName.Substring(fileName.LastIndexOf(‘.’) + 1);
          return AllowedFixFileExtensions.Any(x => x == extension);
          }

          In GetFile you do this:

          if (IsFileFixAllowed(fullPath))
          {
          var fixedView = CorrectView(virtualPath, fileView);
          return new FileResource(virtualPath, fixedView);
          }
          return new FileResource(virtualPath, fileView);

  • spe

    just great.

    how do you deal with the shared models and entities can we have a sample on that part.

    • http://www.gauffin.org jgauffin

      Like with any other area. But you’ll have to put them in a separate class library which is used by multiple plugins.

  • spe

    and can we have dynamic models views(forms) by inspecting the loaded modules @ run time e.g. for managing settings of the loaded plugins.

    each module with publish it’s setting and the composer will build some dynamic model(or the plugins will inject their settings ).

    • http://www.gauffin.org jgauffin

      the plugin areas are no different from regular areas. You can do whatever you like in them.

      Griffin.MvcContrib only does two things:

      a) tells ASP.NET where to find all content files and controllers
      b) Reparse the Views a bit so that they are loaded as any other views.

      So anything possible in a regular MVC app should be possible in your plugins.

      Regarding composing, you might want to check out the comand handling and the domain events in my container: http://www.codeproject.com/Articles/440665/Having-fun-with-Griffin-Container

  • spe

    That is one of the best articles i have ever read. Thank you very much sir.

  • Martin Overgaard

    Hi there,

    Nice article. One question though. Is it posible to use this without areas in a plugin?

    /Martin

    • http://www.gauffin.org jgauffin

      I’ve never tried.

      • Martin Overgaard

        Ok i’ll do that for you, and post back her later on if I get i to work ;o)

      • Martin Overgaard

        Just did a quick test with a simple controller and a simple “I’m online” view and it seems to be working just fine. So if someone else should ask, now you know.

        Have a nice day ;o)

        /Martin

  • Tim Johnson

    Just wondering on your thoughts related to these projects that do something similar:
    1) http://blog.longle.net/2012/03/29/building-a-composite-mvc3-application-with-pluggable-areas/
    2) MVC’s Contrib project with portable areas
    3) http://shazwazza.com/post/Developing-a-plugin-framework-in-ASPNET-with-medium-trust.aspx
    4) http://nileshhirapra.blogspot.com/2012/03/iinstaller-aspnet-mvc-pluggable.html (umbraco developer)

    Would be nice to get your perspective on this. Appreciate in advance.

    • http://www.gauffin.org jgauffin

      nope. You gotto do the benchmarking yourself. Feel free to report back the differences.

  • RongieZeng

    Hi, still don’t know how to put the static files in Plugin class library and refresh without rebulding, Is it now support by Griffin.MvcContrib. If yes, please tell me how to use it, thanks !

  • Lars Ole

    This looks pretty cool. I tried getting it to work with mvc4, but failed. Is that to be expected? (The view is still not found after installing the nuget package and registering stuff in global.asax)

    If the nuget package is indeed mvc3 specific – any hope of an mvc4 compatible version? :)

    Thanks for an interesting article and a very promising looking library.

  • Martin Overgaard

    Hi there,

    I have set up a fully working example of the plugin system, I have one question though. When I add a css file to a plugin and add it to a _Layout.cshtml file and run the site the styles does not get loaded. When I view the source and click on the css file, I get an 404 File not found error. I have marked it as Embedded Resource and I call i like @Url.Content(“~/Areas/Page/Content/page.css”).

    I use the latest source from NuGet and I can’t see why it should not work. When I use IlSpy to check the Plugin.Page.dll the css file is under resources. Am I doing somthing wrong here ?

    /Martin

    • Martin Overgaard

      Found that I had to add the FileProvider to the PluginService as it was not present. Everythig works fine, even in Mvc 4 ;o)