13 November 2015

Castle Windsor trying to resolve all controllers causing error on external modules

Today I was playing with SPEAK trying to make a small app as a module to ease some Page History Visualisation. Well, after creating my rendering, speak app, and my service for the datasource, I decided to try it out on installing the module on one of our existing sitecore 8. This site was using GlassMapper, and using Castle Windsor as Ioc. Well after the successful install, I went and try to access my module and??? I got a 500 coming from my controller:


on my controller I only have something simple and not even using IoC:

       public ActionResult GetCurrentItem(string id, string database, string language, string version)
        {
            PageHistoryService service = new PageHistoryService();
            return Json(service.GetPageHistory(id, database, language, version), JsonRequestBehavior.AllowGet);

        }


Weirdly enough this is actually coming from Castle windsor which is in the existing project.

My Module dll would be with the namespace: XModule.Web
The Project already running in the sitecore would be : XProject.Web

Castle Windsor is defined in the initialisation pipeline as per:

    
      
        
        
        
        
      


This is quite a generic implementation of Castle Windosr and you will find quite documented on the web. Especially around the WindsorControllerFactory which most probably will look like:

using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Castle.MicroKernel;
using System.Collections;

namespace XProject.Web.DependencyResolution
{
    public class WindsorControllerFactory : DefaultControllerFactory
    {
        private readonly IKernel kernel;

        public WindsorControllerFactory(IKernel kernel)
        {
            this.kernel = kernel;
        }

        public override void ReleaseController(IController controller)
        {
            kernel.ReleaseComponent(controller);
        }

        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            
            if (controllerType == null)
            {
                throw new HttpException(404, string.Format("The controller for path '{0}' could not be found.", requestContext.HttpContext.Request.Path));
            }

            return (IController)kernel.Resolve(controllerType);
        }
    }
}

Although this is great and working fine in our project itself since we do register our controller on the project, it seems like it is causing some issue when resolving other controllers from other assembly. We could re-edit the project and add some code to ensure the controller from the module assembly is registered as well but that would mean that every time we want to add a new module then we have to edit our castle implementation on the project and add something like

            container.Register(Classes.FromAssemblyNamed("xproject.web").BasedOn().LifestyleScoped());

Well that does not sounds nice to my hear... I would much prefer to not have to recompile an old project when adding module... so what is happening... well when the request hits sitecore pipeline, it goes to the CastleWindsor initialise... then hit the WindsorControllerFactory... as the new dll from the module is not registered (the castle implementation is on an old project...) then it cannot resolve it and the following line will return an exception:

            return (IController)kernel.Resolve(controllerType);

Although this is nice to see the exception so we know castle cannot resolve. wouldn't it be better to not let it die? Shouldn't we try to resolve it another way? for instance using the base method since we inherit from DefaultControllerFactory from System.MVC? my controller in the module would not even need to get resolved. Should we really enforce everyone putting code on our "Shared" Sitecore instance to register our dll for our implementation of Castle? Well I modified slightly the logic in the GetControllerInstance to include a fail safe: try to resolve using Castle but in case there is an issue, try it with the base:

           try
            {
                return (IController)kernel.Resolve(controllerType);
            }
            catch (Exception ex)
            {
                XProject.Web.SitecoreExtensions.Logging.Logger.Warn("Castle windsor could not resolve this controller");
            }
            return base.GetControllerInstance(requestContext, controllerType);

This will still resolve every controller registered. but it will fail graciously and get passed to the Default resolver which will resolve it correctly and so my module controller gets resolved as well...

Just in case, if you do not like try catch, you may also want to try that:

            IHandler handler = ((IKernelInternal)kernel).LoadHandlerByType((string)null, controllerType, null);
            if (handler == null)
            {
                return base.GetControllerInstance(requestContext, controllerType);
            }
            else
            {
                return (IController)kernel.Resolve(controllerType);
            }


This is pretty much the test in Castle that will throw the Component not found exception...

Also refering to my earlier post: http://sitecorepromenade.blogspot.com.au/2015_05_01_archive.html
You will note that implementing the try catch block or the if statement, you will not need to add the following registration:

            container.Register(Classes.FromAssemblyNamed("Sitecore.Speak.Client").BasedOn().LifestylePerWebRequest());
            container.Register(Classes.FromAssemblyNamed("Sitecore.Mvc").BasedOn().LifestylePerWebRequest());
            container.Register(Classes.FromAssemblyNamed("Sitecore.Mvc.DeviceSimulator").BasedOn().LifestylePerWebRequest());
            container.Register(Classes.FromAssemblyNamed("Sitecore.Marketing.Client").BasedOn().LifestylePerWebRequest());
            container.Register(Classes.FromAssemblyNamed("Sitecore.Client.LicenseOptions").BasedOn().LifestylePerWebRequest());

Having say all of that, This is a fail safe. I think it would be better to register the Sitecore one above so it will not fail to resolve with Castle and it will avoid performance hit on the try action... Same thing is true for the module dll. if your implementation of castle can resolve it there would be less performance hit...

SO it would be a better Castle implementation to be able to add external DLL registration without having to rebuild the project... Surely configuration files can help us there... Well My good friend and Geeky Colleague has a nice post about that which I recommend reading:

http://accordingtothegeek.blogspot.com.au/2015/11/castlewindsor-issue-with-mvc-area.html

5 comments: