Logo

Maarten Balliauw {blog}

ASP.NET, ASP.NET MVC, Azure, PHP, OpenXML, VSTS, ...

About the author

Maarten Balliauw is currently employed as .NET Technical Consultant at RealDolmen. His interests are mainly web applications developed in ASP.NET (C#) or PHP and the Windows Azure cloud platform.
More about me More about me
Send mail E-mail me


ASP.NET MVC Quickly Subscribe to my RSS feed Follow me on Twitter! View Maarten Balliauw's profile on LinkedIn
View Maarten Balliauw's MVP profile

Search

Latest Twitter

    Follow me on Twitter...

    My projects

    Disclaimer

    The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

    © Copyright Maarten Balliauw 2010

    Revised: ASP.NET MVC and the Managed Extensibility Framework (MEF)

    A while ago, I did a blog post on combining ASP.NET MVC and MEF (Managed Extensibility Framework), making it possible to “plug” controllers and views into your application as a module. I received a lot of positive feedback as well as a hard question from Dan Swatik who was experiencing a Server Error with this approach… Here’s a better approach to ASP.NET MVC and MEF.

    kick it on DotNetKicks.com

    The Exception

    Server Error

    The stack trace was being quite verbose on this one:

    InvalidOperationException

    The view at '~/Plugins/Views/Demo/Index.aspx' must derive from ViewPage, ViewPage<TViewData>, ViewUserControl, or ViewUserControl<TViewData>.

    at System.Web.Mvc.WebFormView.Render(ViewContext viewContext, TextWriter writer) at System.Web.Mvc.ViewResultBase.ExecuteResult(ControllerContext context) at System.Web.Mvc.ControllerActionInvoker.InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult) at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass11.<InvokeActionResultWithFilters>b__e() at System.Web.Mvc.ControllerActionInvoker.InvokeActionResultFilter(IResultFilter filter, ResultExecutingContext preContext, Func`1 continuation) at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass11.<>c__DisplayClass13.<InvokeActionResultWithFilters>b__10() at System.Web.Mvc.ControllerActionInvoker.InvokeActionResultWithFilters(ControllerContext controllerContext, IList`1 filters, ActionResult actionResult) at System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName) at System.Web.Mvc.Controller.ExecuteCore() at System.Web.Mvc.ControllerBase.Execute(RequestContext requestContext) at System.Web.Mvc.ControllerBase.System.Web.Mvc.IController.Execute(RequestContext requestContext) at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContextBase httpContext) at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContext httpContext) at System.Web.Mvc.MvcHandler.System.Web.IHttpHandler.ProcessRequest(HttpContext httpContext) at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

    Our exception seemed to be thrown ONLY when the following conditions were met:

    • The View was NOT located in ~/Views but in ~/Plugins/Views (or other path)
    • The View created in our MEF plugin was strong-typed

    Problem one… Forgot to register ViewTypeParserFilter…

    Allright, go calling me stupid… Our ~/Plugins/Views folder was not containing the following Web.config file:

    <?xml version="1.0"?>
    <configuration>
      <system.web>
        <httpHandlers>
          <add path="*" verb="*"
              type="System.Web.HttpNotFoundHandler"/>
        </httpHandlers>

        <!--
            Enabling request validation in view pages would cause validation to occur
            after the input has already been processed by the controller. By default
            MVC performs request validation before a controller processes the input.
            To change this behavior apply the ValidateInputAttribute to a
            controller or action.
        -->
        <pages
            validateRequest="false"
            pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
            pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
            userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
          <controls>
            <add assembly="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" namespace="System.Web.Mvc" tagPrefix="mvc" />
          </controls>
        </pages>
      </system.web>

      <system.webServer>
        <validation validateIntegratedModeConfiguration="false"/>
        <handlers>
          <remove name="BlockViewHandler"/>
          <add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler"/>
        </handlers>
      </system.webServer>
    </configuration>

    Now why would you need this one anyway? Well: first of all, you do not want your views to expose their source code. Therefore, we add the HttpNotFoundHandler for this folder. Next, we do not want request validation to happen again (because this is already done when invoking the controller). Next: we want the MvcViewTypeParserFilter to be used for enabling strong-typed views (more on this by Phil Haack).

    Problem two: MEF’s approach to plugins and ASP.NET’s approach to rendering views…

    When compiling a view, ASP.NET dynamically compiles the markup into a temporary assembly, after which it is rendered. This compilation process knows only the assemblies loaded by your web application’s AppDomain. Unfortunately, assemblies loaded by MEF are not available for this compilation process… I went ahead and checked with Reflector if we could do something about this on ASP.NET side: nope. The main classes we need for this are internal :-( The MEF side could be easily tweaked since its source code is available on CodePlex, but… it’s still subject to change and will be included in .NET 4.0 as a framework component, which would limit my customizations a bit for the future.

    Now let’s describe this problem as one, simple sentence: we need the MEF plugin assembly loaded in our current AppDomain, available for all other components in the web application.

    The solution to this: I want a MEF DirectoryCatalog to monitor my plugins folder and load/unload the assemblies in there dynamically. Loading should be no problem, but unloading… The assemblies will always be locked by my web server’s process! So let’s go for another approach: monitor the plugins folder, copy the new/modified assemblies to the web application’s /bin folder and instruct MEF to load its exports from there. The solution: WebServerDirectoryCatalog. Here’s the code:

    public sealed class WebServerDirectoryCatalog : ComposablePartCatalog
    {
        private FileSystemWatcher fileSystemWatcher;
        private DirectoryCatalog directoryCatalog;
        private string path;
        private string extension;

        public WebServerDirectoryCatalog(string path, string extension, string modulePattern)
        {
            Initialize(path, extension, modulePattern);
        }

        private void Initialize(string path, string extension, string modulePattern)
        {
            this.path = path;
            this.extension = extension;

            fileSystemWatcher = new FileSystemWatcher(path, modulePattern);
            fileSystemWatcher.Changed += new FileSystemEventHandler(fileSystemWatcher_Changed);
            fileSystemWatcher.Created += new FileSystemEventHandler(fileSystemWatcher_Created);
            fileSystemWatcher.Deleted += new FileSystemEventHandler(fileSystemWatcher_Deleted);
            fileSystemWatcher.Renamed += new RenamedEventHandler(fileSystemWatcher_Renamed);
            fileSystemWatcher.IncludeSubdirectories = false;
            fileSystemWatcher.EnableRaisingEvents = true;

            Refresh();
        }

        void fileSystemWatcher_Renamed(object sender, RenamedEventArgs e)
        {
            RemoveFromBin(e.OldName);
            Refresh();
        }

        void fileSystemWatcher_Deleted(object sender, FileSystemEventArgs e)
        {
            RemoveFromBin(e.Name);
            Refresh();
        }

        void fileSystemWatcher_Created(object sender, FileSystemEventArgs e)
        {
            Refresh();
        }

        void fileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
        {
            Refresh();
        }

        private void Refresh()
        {
            // Determine /bin path
            string binPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin");

            // Copy files to /bin
            foreach (string file in Directory.GetFiles(path, extension, SearchOption.TopDirectoryOnly))
            {
                try
                {
                    File.Copy(file, Path.Combine(binPath, Path.GetFileName(file)), true);
                }
                catch
                {
                    // Not that big deal... Blog readers will probably kill me for this bit of code :-)
                }
            }

            // Create new directory catalog
            directoryCatalog = new DirectoryCatalog(binPath, extension);
        }

        public override IQueryable<ComposablePartDefinition> Parts
        {
            get { return directoryCatalog.Parts; }
        }

        private void RemoveFromBin(string name)
        {
            string binPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin");
            File.Delete(Path.Combine(binPath, name));
        }
    }

    Download the example code

    First of all: this was tricky, and the solution to it is also a bit tricky. Use at your own risk!

    You can download the example code here: RevisedMvcMefDemo.zip (1.03 mb)

    kick it on DotNetKicks.com


    Categories: ASP.NET | C# | Debugging | General | MEF | MVC | Personal

    Comments

    DotNetKicks.com | Reply

    Wednesday, June 17, 2009 10:20 AM

    trackback

    Revised: ASP.NET MVC and the Managed Extensibility Framework (MEF)

    You've been kicked (a good thing) - Trackback from DotNetKicks.com

    Juliën Netherlands | Reply

    Wednesday, June 17, 2009 10:34 AM

    Juliën

    Indeed: I KILL YOU for that line of code ;)

    Joe Future United States | Reply

    Tuesday, June 30, 2009 9:38 PM

    Joe Future

    This is pretty slick - thanks for sharing.

    When I implemented this solution, I noticed that the plugin dlls are being copied to my bin directory properly, but the 1st time the page is rendered I'm getting errors because types defined in those plugins aren't able to load.  If I refresh the page again, it all loads fine (presumably because the assemblies are in the bin folder from the start of the IIS process).  Know of any way to prevent this situation?

    Jeff Stalnaker United States | Reply

    Thursday, August 20, 2009 3:56 PM

    Jeff Stalnaker

    Joe,
    Just wondering if you've found a solution to this. I'm having the same issue.

    Thanks,
    Jeff

    David Walker Switzerland | Reply

    Thursday, July 02, 2009 1:03 PM

    David Walker

    in this download of your sample code, the democontroller.cs is always being created by the default controller factory because your metadata specifies "controllerName" but the MefControllerFactory is looking for "ControllerName"

    Ben United Kingdom | Reply

    Thursday, July 09, 2009 5:17 PM

    Ben

    Hello,

    Great solution! We were having the same problem and you code works a treat!

    I took me a while to get it to work, we would have an exception thrown when instanciating DirectoryCatalog. Looking into the exception it was having difficulties loading the System.Web.Mvc assembly.

    As it happens we were using MVC futures and referencing Microsoft.Web.MVC.dll.

    Removing that reference and deleting Microsoft.Web.MVC.dll from the bin folder of the MVC project fixed the problem (it was OK to remove that reference because in the end we ended up using nothing the the MVC futures project).

    Hope this helps!

    Ben

    Joe Future United States | Reply

    Monday, August 17, 2009 1:31 PM

    Joe Future

    It seems that copying files to the bin folder causes the app pool to restart, which causes Refresh() to get called next time I load an extension controller, which copies folders to the bin folder, etc.

    I worked around this problem by adding some post-build lines to my build process to cycle IIS and then drop the built extension binaries into the bin folder myself.  It's a hack for now, but at least my site isn't constantly reloading now.

    Maarten Balliauw {blog} | Reply

    Tuesday, July 27, 2010 2:24 PM

    trackback

    ASP.NET MVC 3 and MEF sitting in a tree...

    ASP.NET MVC 3 and MEF sitting in a tree...

    Add comment




      Country flag

    biuquote
    • Comment
    • Preview
    Loading