Wednesday, September 8, 2010

Using Ninject as IOC container in ASP.NET MVC

Yesterday I tried to use Fluent NHibernate and Ninject in ASP.NET MVC project the first time to prepare for my personal project. After doing some reading and surfing around the Internet, I end up with a solution and it just works. And here is how I did.

First, I start with a Ninject module which is used to register dependencies
public class CustomNinjectModule : NinjectModule
{
public override void Load()
{
Bind<ISessionFactory>().ToMethod(c => GetSessionFactory()).InSingletonScope();
Bind<ISession>().ToMethod(c => c.Kernel.Get<ISessionFactory>().OpenSession()).InRequestScope();
}

public static ISessionFactory GetSessionFactory()
{
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(c => c.FromConnectionStringWithKey("connectionString")))
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<ProductMap>())
.BuildSessionFactory();
}
}
In this custom module, I used FluentNHibernate to create instance of ISessionFactory. We only need one instance of ISessonFactory for the whole project so I keep in singleton scope. In opposite way, we need to create a ISession instance per web request so I set it in request scope.

Then I create a bootstrapper which is used to create the kernel with above module
public static class NinjectBootstrapper
{
public static IKernel Kernel { get; private set; }

public static void Initialize()
{
Kernel = new StandardKernel(new CustomNinjectModule());
}
}
Because I uses Ninject as IOC, in a consistent way it's better if I also use it to create instances of controllers. So I create a custom controller factory

public class NinjectControllerFactory : DefaultControllerFactory
{
private readonly IKernel _kernel;

public NinjectControllerFactory(IKernel kernel)
{
_kernel = kernel;
}

protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
try
{
return _kernel.Get(controllerType) as IController;
}
catch (Exception)
{
return base.GetControllerInstance(requestContext, controllerType);
}
}
}
The last thing I need to do is updating Application_Start in my MvcApplication to initialize dependecies and use the custom factory

protected void Application_Start()
{
NinjectBootstrapper.Initialize();
ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory(NinjectBootstrapper.Kernel));

AreaRegistration.RegisterAllAreas();

RegisterRoutes(RouteTable.Routes);
}
Lesson learn:
  • How to use Ninject to inject dependencies in ASP.NET MVC
  • Understand about some Ninject scopes and how to create instance in the scopes (transient, singleton and request)
  • How to retrieve dependencies from current context (IContext.Kernel.Get<ISessionFactory>())
  • And object lifecycle management in Ninject (as you can see, I don't need to release instances of ISession after each request because Nijnect does it for me. For more details, you can find here)

Tuesday, September 7, 2010

How to test ASP.NET MVC routines with optional parameters

I defined a map route in Global.asax.cs as below
routes.MapRoute(null,
"product/{category}",
new { controller = "Product", action = "List", category = UrlParameter.Optional });
and ProductController's List method
public ActionResult List(string category = null)
{
IList<Product> products;

if (category == null)
{
products = _productRepository.GetAll();
}
else
{
products = _productRepository.GetAllByCategory(category);
}

return View(products);
}
And here are a few attempts to test the route (with MVC Contrib Test Helper support)

First attempt


"~/product".Route().ShouldMapTo<ProductController>(c => c.List());
I have a compile error: "An expression tree may not contain a call or invocation that uses optional arguments". Grrrr, expressions don't play with optional arguments.

Second attempt


"~/product".Route().ShouldMapTo<ProductController>(c => c.List(null));
The test throws a MvcContrib.TestHelper.AssertionException: "Value for parameter 'category' did not match: expected 'System.Web.Mvc.UrlParameter' but was ''."

Third attempt


"~/product".Route().ShouldMapTo<ProductController>(c => c.List(""));
Same result as second attempt. Grrrr...

And the forth attempt


var routeData = "~/product".WithMethod(HttpVerbs.Get);
routeData.Values["category"] = null;
routeData.ShouldMapTo<ProductController>(c => c.List(null));
Now the test pass.

Why? 'category' is an optional parameter, as mentioned here, I expect that I don't need to explicit to define it in route value dictionary and the third attempt should be passed. Is a place to improve MVC Contrib test helper?