Sunday, August 15, 2010

Upgrade MVC2 to MVC3

Today I upgrade a fairly new MVC2 app to MVC3. My original MVC2 app was based off of The Tekpub ASP.NET MVC 2.0 Starter Site

The first step is to update your projects references. This blog post by Rick Schott worked great to handle that task.

After doing that I was faced with the error:
No parameterless constructor defined for this object.

My original global.asax.cs looked like this:
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Ninject.Web.Mvc;
using Ninject;
using Ninject.Modules;
using Site.Infrastructure.Logging;
using Web.Infrastructure.Authentication;
using Web.Infrastructure.Reporting;
using Web.Infrastructure.Storage;
using Web.Model;

namespace Web
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801

    public class MvcApplication : NinjectHttpApplication
    {
        public static ISession Session
        {
            get
            {
                return _container.Get<ISession>();
            }
        }

        public static void RegisterRoutes(RouteCollection routes)
        {
            
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
            routes.IgnoreRoute("favicon.ico");
            routes.MapRoute(
                "Login", // Route name
                "login", // URL with parameters
                new { controller = "Session", action = "Create" } // Parameter defaults
            );
            routes.MapRoute(
                "Logout", // Route name
                "logout", // URL with parameters
                new { controller = "Session", action = "Delete" } // Parameter defaults
            );

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );

        }

        protected override void OnApplicationStarted()
        {
            Logger.Info("App is starting");

            //Database.SetInitializer<SiteDB>(new AlwaysRecreateDatabase<SiteDB>());

            AreaRegistration.RegisterAllAreas();
            RegisterRoutes(RouteTable.Routes);

            //ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory(Container));
        }

        protected void Application_End()
        {
            Logger.Info("App is shutting down");
        }

        protected void Application_Error()
        {
            Exception lastException = Server.GetLastError();
            Logger.Fatal(lastException);
        }

        /// <summary>
        /// IoC stuff below
        /// </summary>
        /// <returns></returns>
        protected override IKernel CreateKernel()
        {
            return Container;
        }

        internal class SiteModule : NinjectModule
        {
            public override void Load()
            {
                //a typical binding
                Bind<ILogger>().To<NLogLogger>().InSingletonScope();
                //Bind<INoSqlServer>().To<DB4OServer>().InSingletonScope();
                //Bind<ISession>().To<Db4oSession>().InRequestScope();

                //You can use the SimpleRepository to build out your database
                //it runs "Auto Migrations" - changing your schema on the fly for you
                //should you change your model. You can switch it out as you need.
                //http://subsonicproject.com/docs/Using_SimpleRepository
                Bind<ISession>().To<SiteEFSession>();
                Bind<IReporting>().To<ReportingSession>();
                Bind<IAuthenticationService>().To<UserAuthenticationService>();
            }
        }

        public ILogger Logger
        {
            get
            {
                return Container.Get<ILogger>();
            }
        }

        static IKernel _container;
        public static IKernel Container
        {
            get
            {
                if (_container == null)
                {
                    _container = new StandardKernel(new SiteModule());
                }
                return _container;
            }
        }
    }
}

I then tried to add the NinjectServiceLocator.cs class and update my global.asax.cs to use Ninject like this sample from Scott Gu.

It looked something like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Ninject;
using Ninject.Modules;
using Site.Infrastructure.Logging;
using Web.Infrastructure.Authentication;
using Web.Infrastructure.Reporting;
using Web.Infrastructure.Storage;
using Web.Model;
using Mvc3Ninject.Utility;

namespace Web
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801

    public class MvcApplication : HttpApplication
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
        }

        public static void RegisterRoutes(RouteCollection routes)
        {
            
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
            routes.IgnoreRoute("favicon.ico");
            routes.MapRoute(
                "Login", // Route name
                "login", // URL with parameters
                new { controller = "Session", action = "Create" } // Parameter defaults
            );
            routes.MapRoute(
                "Logout", // Route name
                "logout", // URL with parameters
                new { controller = "Session", action = "Delete" } // Parameter defaults
            );

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );

        }

        public static void RegisterServices(IKernel kernel)
        {
            //a typical binding
            kernel.Bind<ILogger>().To<NLogLogger>().InSingletonScope();
            //Bind<INoSqlServer>().To<DB4OServer>().InSingletonScope();
            //Bind<ISession>().To<Db4oSession>().InRequestScope();

            //You can use the SimpleRepository to build out your database
            //it runs "Auto Migrations" - changing your schema on the fly for you
            //should you change your model. You can switch it out as you need.
            //http://subsonicproject.com/docs/Using_SimpleRepository
            kernel.Bind<ISession>().To<SiteEFSession>();
            kernel.Bind<IReporting>().To<ReportingSession>();
            kernel.Bind<IAuthenticationService>().To<UserAuthenticationService>();
        }

        public void SetupDependencyInjection()
        {
            // Create Ninject DI Kernel 
            _container = new StandardKernel();

            //// Register services with our Ninject DI Container
            RegisterServices(_container);

            //// Tell ASP.NET MVC 3 to use our Ninject DI Container 
            MvcServiceLocator.SetCurrent(new NinjectServiceLocator(_container));
        }

        void Application_Start()
        {
            SetupDependencyInjection();

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

            Logger.Info("App is starting");
        }

        protected void Application_End()
        {
            Logger.Info("App is shutting down");
        }

        protected void Application_Error()
        {
            Exception lastException = Server.GetLastError();
            Logger.Fatal(lastException);
        }

        public ILogger Logger
        {
            get
            {
                return Container.Get<ILogger>();
            }
        }

        static IKernel _container;
        public static IKernel Container
        {
            get
            {
                return _container;
            }
        }
    }
}

This caused the following error to constantly trigger the debugger although the site seems to still be working:
Error activating IControllerFactory
No matching bindings are available, and the type is not self-bindable.

I then added the following line to the RegisterServices function based on this Stackoverflow tip:
kernel.Bind<IControllerFactory>().To<Ninject.Web.Mvc.NinjectControllerFactory>();

This caused the following error:
Method Ninject.Syntax.IBindingToSyntax`1[System.Web.Mvc.IControllerFactory].To: type argument 'Ninject.Web.Mvc.NinjectControllerFactory' violates the constraint of type parameter 'TImplementation'.

I then tried using DefaultControllerFactory instead of the line above and finally success!
kernel.Bind<IControllerFactory>().To<DefaultControllerFactory>();

This is my final working global.asax.cs file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Ninject;
using Ninject.Modules;
using Site.Infrastructure.Logging;
using Web.Infrastructure.Authentication;
using Web.Infrastructure.Reporting;
using Web.Infrastructure.Storage;
using Web.Model;
using Mvc3Ninject.Utility;

namespace Web
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801

    public class MvcApplication : HttpApplication
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
        }

        public static void RegisterRoutes(RouteCollection routes)
        {
            
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
            routes.IgnoreRoute("favicon.ico");
            routes.MapRoute(
                "Login", // Route name
                "login", // URL with parameters
                new { controller = "Session", action = "Create" } // Parameter defaults
            );
            routes.MapRoute(
                "Logout", // Route name
                "logout", // URL with parameters
                new { controller = "Session", action = "Delete" } // Parameter defaults
            );

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );

        }

        public static void RegisterServices(IKernel kernel)
        {
            kernel.Bind<IControllerFactory>().To<DefaultControllerFactory>();

            //a typical binding
            kernel.Bind<ILogger>().To<NLogLogger>().InSingletonScope();
            //Bind<INoSqlServer>().To<DB4OServer>().InSingletonScope();
            //Bind<ISession>().To<Db4oSession>().InRequestScope();

            //You can use the SimpleRepository to build out your database
            //it runs "Auto Migrations" - changing your schema on the fly for you
            //should you change your model. You can switch it out as you need.
            //http://subsonicproject.com/docs/Using_SimpleRepository
            kernel.Bind<ISession>().To<SiteEFSession>();
            kernel.Bind<IReporting>().To<ReportingSession>();
            kernel.Bind<IAuthenticationService>().To<UserAuthenticationService>();
        }

        public void SetupDependencyInjection()
        {
            // Create Ninject DI Kernel 
            _container = new StandardKernel();

            //// Register services with our Ninject DI Container
            RegisterServices(_container);

            //// Tell ASP.NET MVC 3 to use our Ninject DI Container 
            MvcServiceLocator.SetCurrent(new NinjectServiceLocator(_container));
        }

        void Application_Start()
        {
            SetupDependencyInjection();

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

            Logger.Info("App is starting");
        }

        protected void Application_End()
        {
            Logger.Info("App is shutting down");
        }

        protected void Application_Error()
        {
            Exception lastException = Server.GetLastError();
            Logger.Fatal(lastException);
        }

        public ILogger Logger
        {
            get
            {
                return Container.Get<ILogger>();
            }
        }

        static IKernel _container;
        public static IKernel Container
        {
            get
            {
                return _container;
            }
        }
    }
}

Alternatively in going through this headache I found downloading the source for both Ninject and Ninject.Web.Mvc and updating it to use .NET Framework 4.0 and Mvc 3 fixed most of the errors above and allows you to NOT change the global.asax.cs at all. You can just use it as it was originally.

To get Ninject.Web.Mvc updated you must:
  • Change the Target Framework to .NET 4.0.
  • Update the reference to System.Web.Mvc to use 3.0.0.0 instead of 2.0.0.0
  • Remove the following references:
    • System.Web.Routing
    • System.Web.Abstractions
In closing, I really wish the MVC team would include a default Dependency Injection with MVC. I've lost way too many hours at this point trying to get DI working between all the different options and all the outdated tutorials. If I'm struggling with it I have to imagine there are many others out there as well. This is making it harder for people to adopt MVC and should be eliminated by the MVC team picking a default to include when installing MVC and generating the default global.asax.