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?

Saturday, July 31, 2010

More room to work

One 24" monitor seems is too cramped for me, especially when I plan to do some design things, so I decide to buy another one. It must be a Dell UltraSharp because currently I'm using 2405 FPW and I'm in love with it.

An UltraSharp 24" monitor is too expensive for me (about 13.5 million VietNam dong, and next month I need to spend a lot of money for my daughter, my wife will give a birth this September), so a 23" one which costs only a half is fitting my budget. That's why I decide to buy a U2311H.


As you can see, the U2311H is slightly smaller than the 2405 FPW. The color of U2311H seems a bit colder than in 2405 FPW. But over all, I satisfy with it.

Now is the time to work :-)

Thursday, June 3, 2010

Getting started with BDD

As maybe all of us already knew, in TDD (Test Driven Development), before writing code we should write a test. The test makes sure that we write code right, we will not break other units, and beyond the test helps us to think about design before writing the code. But all these things are in internal of the software, or the low level, they’re not what user can look, feel, touch any maybe even care about. User cares about how the software response their behaviours. It’s something likes “When I click this button, it should show a message box.” Showing message box is what user expected. If the message isn’t shown, it must be something wrong in the software what user didn’t expect. BDD (Behaviour Driven Development) comes to solve this problem.

When we wanna implement a new feature, we should start from an acceptance test. The acceptance test should contains all possible scenarios to make sure when it is passed, there is a sign for us that the feature is completed. And BDD is good way to write acceptance tests.

Theory about BDD, you can find out here but in this post, I will show you a small example how to do BDD with SpecFlow and ASP.NET MVC. The nice thing is SpecFlow provides Visual Studio template and you also can run the test with your test runner, like ReSharper or Test Driven .NET.

In this example, we will test when user opens Product page, the application should open it, not the other one. To do this example, I assume that you already install SpecFlow on your PC. If you didn’t have SpecFlow yet, you can grab it from its web site and it’s quite easy to install. Let’s start.

First we create an empty solution, and name it BddDemo.


Figure 1

Then we add a new ASP.NET MVC 2 Web Application with name BddDemo.Web.


Figure 2

Then we add a new Class Library project with name BddDemo.Specs.


Figure 3

Remove Class1.cs, it’s redundant.

Add reference to SpecFlow, NUnit, System.Web.Mvc and BddDemo.Web.


Figure 4

Add BrowseProductPage.feature by using SpecFlowFeature template.


Figure 5

Replace the content of BrowseProductPage.feature by this fragment and save it.

Feature: Browse product page

Scenario: Browse product page
When I open product page
Then the product page should be displayed

Open BrowseProductPage.feature.cs, you should see the test code. Try to run it, the test should be ignored because we didn’t implement test steps yet.


Figure 6

Add BrowseProductPageSteps.cs by using SpecFlowStepDefinition template to implment test steps.


Figure 7

Replace content of BrowseProductPageSteps.cs by this code fragment as save it.
using TechTalk.SpecFlow;

namespace BddDemo.Specs
{
[Binding]
public class BrowseProductPageSteps
{
[When("I open product page")]
public void WhenIOpenProductPage()
{
//TODO: implement act (action) logic

ScenarioContext.Current.Pending();
}

[Then("the product page should be displayed")]
public void ThenTheProductPageShouldBeDisplayed()
{
//TODO: implement assert (verification) logic

ScenarioContext.Current.Pending();
}
}
}

Run the test once again, it is still ignored because we didn’t have any logic yet. Now it’s time to write it. Replace content of BrowseProductPageSteps.cs by below fragment as save it.
using System.Web.Mvc;
using BddDemo.Web.Controllers;
using NUnit.Framework;
using TechTalk.SpecFlow;

namespace BddDemo.Specs
{
[Binding]
public class BrowseProductPageSteps
{
private readonly ProductController _controller = new ProductController();
private ViewResult _result;

[When("I open product page")]
public void WhenIOpenProductPage()
{
_result = _controller.List();
}

[Then("the product page should be displayed")]
public void ThenTheProductPageShouldBeDisplayed()
{
Assert.IsEmpty(_result.ViewName, "Wrong view name.");
Assert.AreEqual("Product", _controller.ViewData["title"], "Wrong page was shown.");
}
}
}

If we try to compile the project now, for sure, it will fail because we don’t have ProductController yet. Let’s add it to BddDemo.Web project. Right click on Controllers folder in BddDemo.Web project the click Add, Controller. In Add Controller dialog, please name controller as ProductController.


Figure 8

Replace ProductController.cs content by
using System.Web.Mvc;

namespace BddDemo.Web.Controllers
{
public class ProductController : Controller
{
public ViewResult List()
{
ViewData["title"] = "Product";

return View();
}

}
}

To keep it simple, I don’t implement any special logic for ProductController’s List(). I only set value for the “title” view data, this one will be used to validated that it’s Product page.

As we didn’t have List view, ReSharper asks us to create it. Press Alt + Enter on “View()”, then select Create view ‘List’, then just click OK in Creating ASP.NET MVC View dialog. The List view will be created.

Back to BrowseProductPageSteps.cs, I use ReSharper quick fix to import BddDemo.Web.Controllers namespace.

Now try to run the test, it should pass and your customer should be happy.


Figure 9


You can download the source code for this post here: BddDemo.zip

Update: Link to source code

Monday, May 31, 2010

Hello, World

while (AgileHobo.IsAlive) {
    DoAgility();
}