1

I have MVC areas in external libraries which have their own area registration code just as a normal MVC area would. This area registration gets called for each dll (module) and I have verified the RouteTable contains all the routes from the loaded modules once loading has been completed.

When I reference these external areas in the main site they get pulled into the bin directory and load up fine. That is, when a request is made for a route that exists in an external library, the correct type is passed to my custom controller factory (Ninject) and the controller can be instantiated.

Once I move these dll's outside of the bin directory however (say to a Modules folder), there appears to be an issue with routing. I have checked that the RouteTable has all the required routes but by the time a request makes its way into the ninject controller factory the requested type is null. From reading here an SO link here this behaviour seems to occur when ASP.NET MVC cannot find the controller matching the requested route or does not know how to make sense of the route.

When loading the modules externally I have ensured that the modules that I want loaded are loaded into the app domain via a call to Assemby.LoadFrom(modulePath);

I did some research and it appears that when attempting to load a library outside of bin you need to specify private probing in app.config as pointed out here;. I have mine set to 'bin\Modules' which is where the mvc area modules get moved too.

Does anyone have any ideas why simply moving an mvc area project outside of the bin folder would cause the requested type passed into the controller factory to be null resulting in the controller to be instantiated?

Edit:

  • All routes registered in external areas have the namespace of the controller specified in the route

Below is a fragment of code that creates a new Ninject kernel, reads a list of module names from a file to enable, and then goes searching for the enabled modules in the bin/Modules directory. The module is loaded via the assembly loader, has its area(s) registered and then loaded into the ninject kernel.

        // comma separated list of modules to enable
        string moduleCsv = ConfigurationManager.AppSettings["Modules.Enabled"];
        if (!string.IsNullOrEmpty(moduleCsv)) {
            string[] enabledModuleList = moduleCsv.Replace(" ", "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
            _Modules = enabledModuleList ?? new string[0];

            // load enabled modules from bin/Modules.
            var moduleList = Directory.GetFiles(Server.MapPath("~" + Path.DirectorySeparatorChar + "bin" + Path.DirectorySeparatorChar + "Modules"), "*.dll");

            foreach (string enabledModule in enabledModuleList) {
                string modulePath = moduleList.Single(m => m.Contains(enabledModule));
                // using code adapted from from AssemblyLoader
                var asm = AssemblyLoader.LoadAssembly(modulePath);
                // register routes for module
                AreaRegistrationUtil.RegisterAreasForAssemblies(asm);

                // load into Ninject kernel
                kernel.Load(asm);
            }
        }

This is the crux of the Ninject controller factory that receives the aforementioned Ninject kernel and handles requests to make controllers. For controllers that exist within an assembly in bin/Modules the GetControllerType(...) returns null for the requested controller name.

public class NinjectControllerFactory : DefaultControllerFactory
{
    #region Instance Variables
    private IKernel _Kernel;
    #endregion

    #region Constructors
    public NinjectControllerFactory(IKernel kernel)
    {
        _Kernel = kernel;
    }

    protected override Type GetControllerType(System.Web.Routing.RequestContext requestContext, string controllerName)
    {
        // Is null for controller names requested outside of bin directory.
        var type = base.GetControllerType(requestContext, controllerName);
        return type;
    }

    protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
    {
        IController controller = null;
        if (controllerType != null)
            controller = _Kernel.Get(controllerType) as IController;
        return controller;
    }
}

Update on Ninject Nuget Install

I couldn't get it to install Ninject.MVC3 via NuGet for some reason. Visual Studio was giving some schemaVersion error when clicking the install button (I have installed other Nuget packages like ELMAH btw).

I did find out something else that was interesting though, and that is that if I pass in the extra module assembilies to the NinjectControllerFactory I have and search those when the type cannot be resolved it finds the correct type and is able to build the controller. This leads to another strange problem.

The first route to be requested from an external module is the /Account/LogOn in the auth and registration module. The virtual path provider throws an error here after it has located the view and attempts to render it out complaining of a missing namespace. This causes an error route to fire off which is handled by an ErrorHandling module. Strangely enough, this loads and render fine!

So I am still stuck with two issues; 1) Having to do a bit of a dodgy hack and pass in the extra module assemblies to the NinjectControllerFactory in order to be able to resolve types for Controllers in external modules 2) An error with one particular module where it complains about a namespace not being found

These two issues are obviously connected because the assembly loading just isn't loading up and making everything available that needs to be. If all these mvc areas are loaded from the bin directory everything works fine. So it is clearly a namespacing/assembly load issue.

Community
  • 1
  • 1
Joshua Hayes
  • 1,938
  • 2
  • 21
  • 39

1 Answers1

1

LoadFrom load the assembly into the loading context. These types are not available to the other classes in the default Load context. Probably this is the reason why the controller is not found.

If you know which assemblies have to be loaded then you should always use Assembly.Load(). If you don't know which assemblies are depolyed in the directory then either guess from the filesnames the assembly names or use Assembly.ReflectionOnlyLoadFrom() (preferably using a temporary AppDomain) to get the assembly names. Then load the assemblies using Assembly.Load() with the assembly name.

If your assemblies contain NinjectModules you can also use kernel.Load() which does what I described above. But it only loads assemblies containing at least one module.

Read up http://msdn.microsoft.com/en-us/library/dd153782.aspx about the different assembly contexts.

Here is a small extract from the Ninject codebase. I removed the unnecessary stuff but did not try to compile or run so probably there are minor issues with this.

public class AssemblyLoader
{
    public void LoadAssemblies(IEnumerable<string> filenames)
    {
        GetAssemblyNames(filenames).Select(name => Assembly.Load(name));
    }

    private static IEnumerable<AssemblyName> GetAssemblyNames(IEnumerable<string> filenames)
    {
        var temporaryDomain = CreateTemporaryAppDomain();
        try
        {
            var assemblyNameRetriever = (AssemblyNameRetriever)temporaryDomain.CreateInstanceAndUnwrap(typeof(AssemblyNameRetriever).Assembly.FullName, typeof(AssemblyNameRetriever).FullName);

            return assemblyNameRetriever.GetAssemblyNames(filenames.ToArray());
        }
        finally
        {
            AppDomain.Unload(temporaryDomain);
        }
    }

    private static AppDomain CreateTemporaryAppDomain()
    {
        return AppDomain.CreateDomain(
            "AssemblyNameEvaluation",
            AppDomain.CurrentDomain.Evidence,
            AppDomain.CurrentDomain.SetupInformation);
    }

    private class AssemblyNameRetriever : MarshalByRefObject
    {
        public IEnumerable<AssemblyName> GetAssemblyNames(IEnumerable<string> filenames)
        {
            var result = new List<AssemblyName>();
            foreach(var filename in filenames)
            {
                Assembly assembly;
                try
                {
                    assembly = Assembly.LoadFrom(filename);
                }
                catch (BadImageFormatException)
                {
                    // Ignore native assemblies
                    continue;
                }

                result.Add(assembly.GetName(false));
            }

            return result;
        }
    }
}
Remo Gloor
  • 32,665
  • 4
  • 68
  • 98
  • Hi Remo. I added your AssemblyLoader code and adapted it to allow loading an individual assembly. Loading the assembly this way (via Assembly.Load(name) however doesn't seem to make any difference to the NinjectControllerFactory when it attempts to resolve the controller type given a controlelr name. The other thing which perplexes me is that I have added the bin/Modules folder on the private probing path so what have thought these assemblies would be available? – Joshua Hayes Sep 20 '11 at 01:49
  • @Joshua Hayes Configuring a probing path does nothing else than that this directory is added to the probing paths for assemblies loaded using Assembly.Load(). It changes nothing for assemblies loaded using LoadFrom(). Also the assemblies are not loaded into the appdomain automatically if they are in the probing path. – Remo Gloor Sep 20 '11 at 08:46
  • @Joshua Hayes Have you tried Ninject.MVC3 extension instead of your own factory? There were issues where some peoples had problems using areas even within the main assembly with their custom factory. – Remo Gloor Sep 20 '11 at 08:48
  • I haven't checked Ninject.MVC3. I might check it out and see if that helps. Everything works perfectly when the assemblies are loaded from bin but the moment they are moved to bin/Modules there is issues. Not sure how ninject MVC 3 would fix this but I'll take a look and report back. – Joshua Hayes Sep 20 '11 at 11:24
  • I should also add that I am already extending a base class that inherits from HttpApplication in our framework code and cannot use the ninject application base for global.asax as I think the MVC extension requires. So I'm not sure that this will solve anything in my situation. I will still take a look at the extensions source however to see if anything different is going on. – Joshua Hayes Sep 20 '11 at 11:41
  • No the extension does not have this limit. Use the 2nd approach from https://github.com/ninject/ninject.web.mvc/wiki/Setting-up-an-MVC3-application – Remo Gloor Sep 20 '11 at 14:49
  • Didn't have any lucky installing via NuGet. As the module registration and kernel loading happens elsewhere in my framework I don't think it would have been that convenient anyway. I did discover another related problem however which I have documented at the bottom of my question as comment space is limited. – Joshua Hayes Sep 21 '11 at 02:32