14

I am having trouble resolving 404 responses in my Asp.Net MVC 4 project. It's built in VS2012 targeting 4.5.

I have pre-compiled views and controllers built into stand-alone DLLs. I am able to dynamically load the DLLs and inspect them from my core project, even invoke methods on them; however, it seems that the MVC Framework is not aware of the controllers. I am close here, but there is something missing.

Background on the Controllers and Views

Controllers are built in a stand-alone MVC project and inherit from Controller. Nothing too interesting going on there. The views use RazorGenerator and become classes that live in the project.

The output of the project is a DLL which correctly contains the controllers and views.

The DLLs implement a specific interface, we'll call it IPlugin, in a separate class (not part of a controller) in the library.

Loading the DLLs

Running as admin in Visual Studio I compile my app, which is hosted under IIS. With the project built, I drop a plugin DLL into my "Plugins" directory. Without debugging (this becomes important later), I open IE and navigate to the site. Note that at this point the App has been built, but never run, so startup events will fire. Everything here is still consistent if I recycle the app pool.

I have a Startup class with two methods, PreStart and PostStart and invoke the methods using WebActivator.PreApplicationStartMethod and WebActivator.PostApplicationStartMethod respectively.

PreStart is where I do the following:

  • Get a list of all the plugin DLLs in my "Plugins" directory
  • Copy all plugins to AppDomain.CurrentDomain.DynamicDirectory
  • Load the type...if it contains an IPlugin I then
    • Add the assembly to the BuildManager
    • Call some of the methods on the class that implements IPlugin

In 'PostStart' I do this bit of code (based on code from RazorGenerator.Mvc):

foreach (var assembly in Modules.Select(m=>m.Value))
{
    var engine = new PrecompiledMvcEngine(assembly)
    {
        UsePhysicalViewsIfNewer = HttpContext.Current.Request.IsLocal
    };

    ViewEngines.Engines.Insert(0, engine);
    VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
}

Modules in this context is a key/value pair where the values are the loaded assemblies. The purpose of this code is to make sure that MVC is aware of the views by adding a view engine for each assembly that knows how to resolve the views (this is part of RazorGenerator).

How I Know I'm Close (but Clearly Lacking the Cigar)

IPlugin defines a method called RegisterRoutes where, you guessed it, routes are to be registered for those who implement the interface. I call this method in PreStart and routes are added - I have verified that these exist in my route table. For instance, on a route defined in my plugin, created through dynamic invocation of the method during the PreStart, I see something like this as a DataToken when examining my routes:

Namespaces = Plugin.Name.Controllers

So, the route is registered, the assembly is loaded, I have verified that the DLL is correctly copied to the DynamicDirectory of the AppDomain. I am able to invoke members of classes that are loaded dynamically at runtime. But when I navigate to the URL that is matched by the route I get a 404. This is not a "could not locate view" YSOD, it's more akin to not finding the controller at all.

Here is the part that confuses the heck out of me: if, at this point, without doing anything, I return to Visual Studio and hit F5...everything works.

It's like Visual Studio is becoming aware of the controller in some way that I can't identify, and the MVC Framework is picking up on it.

Finally, A Question

What am I missing, and how do I get the MVC Framework to be aware of my controller?

And hey, at this point, if you're still reading this, thanks. :)

MisterJames
  • 3,306
  • 1
  • 30
  • 48
  • 1. Is VS running using Cassini? Try changing it to IIS Express and check if it continues to function correctly. 2. Try installing [RouteDebugger](http://nuget.org/packages/routedebugger) - maybe it might give you some clues on whether the routes are being correctly registered under IIS – Pranav Feb 18 '13 at 06:24
  • Thanks @Pranav, but it's on IIS already. Route debugger shows that the routes are working. – MisterJames Feb 18 '13 at 17:12
  • 1
    Could this be an issue? http://stackoverflow.com/questions/14971895/using-precompiledmvcengine-findview-throws-invalidoperationexception-and-looks-f – Tengiz Feb 22 '13 at 17:29
  • +1, but no, not yet, anyways. The framework isn't finding the controller, so there's nothing looking for the view yet. And, again, when I recompile, everything works fine. – MisterJames Feb 23 '13 at 22:57

3 Answers3

8

Turns out that this is a bug in Asp.Net itself.

After discussing the issue with Eilon Lipton of the Asp.Net team, and thinking this was something amiss in the MVC Framework, Eilon and a couple team members dug into things and found that the error was at a lower level per this conversation: http://aspnetwebstack.codeplex.com/discussions/403529

They also suggested a workaround that included another call on BuildManager after the call to AddReferencedAssembly, which I implemented via the following code:

    // Add the plugin as a reference to the application
    BuildManager.AddReferencedAssembly(assembly);
    BuildManager.AddCompilationDependency(assembly.FullName);

This allows you to add additional controllers/compiled views at startup in your pre-application init stage. What I'm doing now is looping through the list of DLLs in my plugins directory and pushing them to BuildManager as above.

The only limitation here is that you can't remove assemblies or clear the cache dynamically. The only way that I've found to do this is to add a previously unknown assembly to the referenced assemblies and compilation dependencies. I am experimenting with dynamically emitting a new assembly during pre-application initialization so that I can always, effectively, clear the cache and remove previously included plugins by faking a new assembly.

Hope this helps someone else out there.

Cheers.

MisterJames
  • 3,306
  • 1
  • 30
  • 48
  • Thank you very much for your detailed question and answer. One thing I don't get is why are you not using a custom controller factory (see `ControllerBuilder.Current.SetControllerFactory`)? This would allow you to have full control over when to yield which controller, wouldn't it? – Dejan Oct 21 '15 at 14:45
  • I tried using a controller factory but it didn't work (404 error), but in Visual Studio in debug works perfectly. With this solution it works. – PLopes Dec 11 '19 at 17:49
1

It looks like this issue:

MVC uses assembly-qualified type name of the view engine to disambiguate view cache entries from different view engines. So it's not possible to have more than one PrecompiledMvcEngine object (as when you have precompiled views in more than one assembly). The problem can be solved by creating a different derived class from PrecompiledMvcEngine for each assembly. Or by creating a single generic derived class parameterized with some type from the assembly.

Article is here.

Nenad
  • 24,809
  • 11
  • 75
  • 93
  • Thanks Nenad, but at this time I am only running one plugin, so there's only one trying to load. This may factor in, but I've also had 2 plugins running at once, so I'm not certain it will. Also, this doesn't address the problem I'm having in that the controller is not located. This is the key to the bounty. Cheers. – MisterJames Feb 22 '13 at 15:27
  • I wish there were. All I'm getting is 404 on the controller when I try to access a view that is . I have Log4Net and ELMAH going and there is nothing going wrong that I'm picking up (with logging at every step). I have more details that I will add to the question later after talking through some things with some members of the Asp.Net team this week. – MisterJames Feb 22 '13 at 15:45
0

@MisterJames, take a look at this:

Asp.Net Mvc Pluggable Application

I hope it's useful.

MisterJames
  • 3,306
  • 1
  • 30
  • 48
Felipe Miosso
  • 7,309
  • 6
  • 44
  • 55
  • Felipe, this is using areas in project from within the same solution and is a completely different approach. My needs are to have stand-alone solutions for the plugins. I'll eval this in more detail to see if there's any clues, but as it stands I don't believe it will help in my scenario. – MisterJames Feb 22 '13 at 15:30