Friday 23 December 2011

Writing ASP.NET MVC 3 Generic Controllers

I am currently working on a project that has an awful lot of data analysis pages built on similar datasets. We decided to build a page model based on generic functionality, which means pages build themselves based on some metadata stored in an SQL Server 2008 database. The following is the planned task flow
  • Figure out a URL structure that'll contain information on the resource or page the user has requested.
  • Use the MVC 3 routing mechanism to resolve the page identity in the URL.
  • Use the page identity to resolve an existing strongly-typed data class authored to maintain datasets for this page.
  • Use the MVC 3 controller resolver to create an instance of a generic controller that takes the data class as a parameter.
  • Write generic methods/routines in the generic controller that gets datasets from the database depending on the data class type.
Lets work our way down the flow.

First, in the Global.asax add a new route that takes an extra parameter.

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"GenericController", // Route name
"Generic/{action}/{GenericControllerVariable}/{id}",
new { controller = "Generic", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);

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

}

Notice how in the "GenericController" route, there is an extra parameter "GenericControllerVariable". Now we've established a way to retrieve or resolve the dataset requested, using the "GenericControllerVariable" as the page identity.

Now we want to resolve the generic controller type and assign it the data class type as a parameter.

public class CustomControllerFactory : DefaultControllerFactory
{
public override IController CreateController(RequestContext requestContext, string controllerName)
{
if (controllerName == "Generic")
{
//Use your favourite DI Container to resolve the customcontrollerfactory
var genericControllerResolver = new CustomGenericControllerFactory();
return genericControllerResolver.GetControllerType(requestContext);

}

return base.CreateController(requestContext, controllerName);
}

protected override System.Type GetControllerType(RequestContext requestContext, string controllerName)
{
if(controllerName == "Generic")
{
return typeof (GenericController<SomeModelType>);
}


return base.GetControllerType(requestContext, controllerName);
}
}

//This should implement an interface
public class CustomGenericControllerFactory
{
public IController GetControllerType(RequestContext requestContext)
{
//Use your favourite DI container to set up and resolve the concrete controller type using the
//following genericControllerVariable.
var genericControllerVariable = requestContext.RouteData.Values["GenericControllerVariable"];

switch (genericControllerVariable.ToString())
{
case "Foo":
return new GenericController<FooType>(new GenericRepository<FooType>());
case "Bar":
return new GenericController<BarType>(new GenericRepository<BarType>());
}

return new GenericController<SomeModelType>(new GenericRepository<SomeModelType>());
}
}

The code above intercepts calls into the default controller creation pipeline, retrieves the "GenericControllerVariable" value and uses this to decide and instantiate the generic controller and it's underlying type. This is made possible with the implementation of a custom dependency resolver, to replace the DefaultControllerFactory with our custom one like so;

public class CustomDependencyResolver : IDependencyResolver
{
public object GetService(Type serviceType)
{
//Use your favourite DI container to set up and resolve the concrete controller type using the
//following genericControllerVariable.
//var genericControllerVariable = requestContext.RouteData.Values["GenericControllerVariable"];

if(serviceType.Name == "IControllerFactory")
{
return new CustomControllerFactory();
}


switch (serviceType.Name)
{
case "Foo":
return new GenericController<FooType>(new GenericRepository<FooType>());
case "Bar":
return new GenericController<BarType>(new GenericRepository<BarType>());
}

return null;
//return new GenericController<SomeModelType>(new GenericRepository<SomeModelType>());
}

public IEnumerable<object> GetServices(Type serviceType)
{
return new List<object>();
}
}

Now register this implementation in the Global.asax.cs file like so;

protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);

DependencyResolver.SetResolver(new CustomDependencyResolver());
}

With the generic controller and it's underlying type resolved, we can go ahead and write our custom generic controller to process requests from the user.

public class GenericController<T> : System.Web.Mvc.Controller
{
private readonly IGenericRepository<T> _genericRepository;

public GenericController(IGenericRepository<T> genericRepository)
{
_genericRepository = genericRepository;
}

public ActionResult Index()
{
return View(_genericRepository.GetData());
}
}

You can download the source code for this demo on GitHub.