2008-02-01

Inversion of control

As you may already know I am writing a website. I've chosen to use MonoRail for the web part and ECO for the persistence. Today has been great fun! I have modified the Castle.MonoRail.EcoSupport library with the following enhancements.

  1. You can now specify a [DefaultEcoSpaceType(typeof(MyEcoSpace))] on either the class or method.
  2. On the EcoDataBind reflection attribute you can now specify as little as [EcoDataBind("Product")] on your method parameter, this will use the specified DefaultEcoSpaceType specified, or throw an exception if no default was specified.
This lets me write code like this
[AllowEcoSpaceDeactivateDirty(true)]
[UseEcoSpacePool(false)]
[UseEcoSpaceSession(EcoSpaceStrategyHandler.SessionStateMode.Never)]
public class ProductAdminController : BaseController
{
  public void Create()
  {
    PropertyBag["Product"] = new Product(GetEcoSpace<MyWebSiteEcoSpace>());
    RenderView("Modify");
  }
}
I can now easily create actions Create/Modify/Edit which all render the same view "Modify.vm". The method signatures for Edit and Modify are as follows
public void Modify(string id)
public void Modify([EcoDataBind("Product", Allow="Name", NoObjectIdAction=ObjectIdAction.CreateNewInstance)]Product product)
The EcoDataBind attribute additionally says that only "Name" should be applied to the object, this is to stop people from posting custom requests with Product.IsActive set to "false" for example, so everything except Name will be ignored. It also states that if there is no Product.ExternalId in the form (there wont be for new objects) then a new Product instance should be created.

But now the fun part. Instead of writing code like this to get a product by its name
IEnumerable<Product> products =
GetDefaultEcoSpace().Ocl.Evaluate("Product.allInstances->orderBy(name)").GetAsIList<Product>();
PropertyBag["Products"] = products;
Maybe it would be better to have a data-access-layer so that I don't have to hard-code that OCL whenever I want a list of products? Sounds good....
public interface IProductProvider
{
  IEnumerable<Product> GetAll();
  Product GetSingleByName(string name);
  IEcoServiceProvider ServiceProvider { get; set; }
}
Now the code is written as follows
IProductProvider productProvider = new MyImplementationProductProvider();
productProvider.ServiceProvider = GetDefaultEcoSpace();
PropertyBag["Product"] = productProvider.GetSingleByName(id);
Okay, so now I have a generic way of getting a list of products ordered by name and a single product by its name. What next? Well seeing as I am writing "proper" code these days I thought I'd add some unit testing in there. After having Shamresh from Inspiration Matters talk so enthusiastically about Inversion of Control with me on Skype over the last month or so I thought I'd take a look at that.

Here's how it works. First I put some XML into my web.config
<component id="IProductProvider" 
  service="MyWebsite.Services.IProductProvider, MyWebsite"
  type="MyWebsite.Services.Implementation.ProductProvider, MyWebSite"/>


What this does is to register a type for a service. It says that the class ProductProvider should be used whenever I ask for IProductProvider. Here is how my method implementation changes...
public void Create()
{
  PropertyBag["Product"] = new Product(GetEcoSpace<MyWebSiteEcospace>());
  RenderView("Modify");
}

public void List()
{
  IProductProvider productProvider = WindsorContainer.Resolve<IProductProvider>();
  productProvider.ServiceProvider = GetDefaultEcoSpace();
  PropertyBag["Products"] = productProvider.GetAll();
}

public void Modify(string id)
{
  IProductProvider productProvider = WindsorContainer.Resolve<IProductProvider>();
  productProvider.ServiceProvider = GetDefaultEcoSpace();
  PropertyBag["Product"] = productProvider.GetSingleByName(id);
}

public void Modify([EcoDataBind("Product", Allow="Name", NoObjectIdAction=ObjectIdAction.CreateNewInstance)]Product product)
{
  GetEcoSpace().UpdateDatabase();
  RedirectToAction("List");
}
As you can see I now use WindsorContainer (from www.castleproject.org) to retrieve my IProductProvider instead of creating it directly. You might be wondering what the point of doing that is? The point is that later when it comes to running my unit tests I can configure the WindsorContainer to create a different class to supply IProductProvider. This means that I can have a class specific to my test that returns 3 products that I create on the spot with specific names, then I can inspect the output of the view and check that they are all being rendered.

This is just one example. The main idea is that my unit tests can control what data the controller actions are given so that I can ensure a known data state during the tests instead of having to rely on having certain objects in my database.

It's been good fun, I hope I see some more places I can implement IoC during the development of this website.

1 comment:

Dmitriy Nagirnyak said...

You can simplify it even more using MonoRail-Windsor Integration.

Also generics usage for EcoSpace can simplify the code too.

So your controller would look like this




public class ProductAdminController : BaseEcoController<MyWebSiteEcoSpace>
{
private readonly IProductProvider productProvider;
public ProductAdminController(IProductProvider productProvider)
{
this.productProvider = productProvider;
}

public void Create()
{
PropertyBag["Product"] = new Product(EcoSpace);
RenderView("Modify");
}

public void List()
{
PropertyBag["Products"] = productProvider.GetAll();
}

public void Modify(string id)
{
PropertyBag["Product"] = productProvider.GetSingleByName(id);
}

public void Modify([EcoDataBind("Product", Allow="Name", NoObjectIdAction=ObjectIdAction.CreateNewInstance)]Product product)
{
EcoSpace.UpdateDatabase();
RedirectToAction("List");
}
}

It also allows writing better tests against the controller because of you can pass your own IProductProvider there.