While I am thoroughly enjoying MVC and autofac, I feel there is something missing when it comes to handling areas. I found areas an excellent idea for segregating larger web apps into manageable chunks when using MonoRail a few years ago. I was unsure of how to incorporate them into MVC until I found Phil Haack’s blog entryand subsequent followup by Steve Sanderson.
This made for a great start. One large problem: controller names had to be unique across every area. At first I resigned myself to having *really* long class names. But then I came to my senses and started hunting through the code for autofac to figure out how to change this. After a few hours of trial and error I figured out what changes needed to be made.
Create a new AreaAwareControllerFactory that extends the built-in factory:
public class AreaAwareControllerFactory : AutofacControllerFactory
{
public AreaAwareControllerFactory(IContainerProvider containerProvider)
: base(containerProvider)
{
}
public AreaAwareControllerFactory(IContainerProvider containerProvider,
IControllerIdentificationStrategy controllerIdentificationStrategy)
: base(containerProvider, controllerIdentificationStrategy)
{
}
public override IController CreateController(RequestContext context, string controllerName)
{
var area = (string)context.RouteData.Values["area"];
if (area != null)
{
controllerName = area + "." + controllerName;
}
return base.CreateController(context, controllerName);
}
}
The next step is to create an area aware identification strategy:
public class AreaAwareControllerIdentificationStrategy : IControllerIdentificationStrategy
{
private IList areaRoots;
private const string Prefix = "controller.";
private const string TypeNameSuffix = "Controller";
public AreaAwareControllerIdentificationStrategy(params string[] areaRoots)
{
this.areaRoots = new List();
foreach (string area in areaRoots)
{
if (!area.EndsWith("."))
{
this.areaRoots.Add(area + ".");
}
else
{
this.areaRoots.Add(area);
}
}
}
public Service ServiceForControllerName(string controllerName)
{
if (controllerName == null)
{
throw new ArgumentNullException("controllerName");
}
if (controllerName == "")
{
throw new ArgumentException("controllerName");
}
return new NamedService(Prefix + controllerName.ToLowerInvariant());
}
public Service ServiceForControllerType(Type controllerType)
{
string serviceName = controllerType.FullName;
foreach(string root in areaRoots)
{
if (serviceName.Contains(root))
{
serviceName = controllerType.FullName.Replace(root, "").Replace(TypeNameSuffix, "");
return ServiceForControllerName(serviceName);
}
}
return ServiceForControllerName(controllerType.Name.Replace(TypeNameSuffix, ""));
}
}
The final step is to wire up the module and factory:
private void RegisterContainer()
{
var builder = new ContainerBuilder();
builder.RegisterModule(new AutofacControllerModule(Assembly.GetExecutingAssembly())
{
IdentificationStrategy =
new AreaAwareControllerIdentificationStrategy("ThirdFamily.Web.Controllers")
});
containerProvider = new ContainerProvider(builder.Build());
ControllerBuilder.Current.SetControllerFactory(new AreaAwareControllerFactory(ContainerProvider));
}
I also made a minor change to where views are located. Replace the original functions in AreaViewEngine with the following:
private static string FormatViewName(ControllerContext controllerContext, string viewName)
{
string controllerName = controllerContext.RouteData.GetRequiredString("controller");
string area = controllerContext.RouteData.Values["area"].ToString();
return "Views/" + area + "/" + controllerName + "/" + viewName;
}
private static string FormatSharedViewName(ControllerContext controllerContext, string viewName)
{
string area = controllerContext.RouteData.Values["area"].ToString();
return "Views/Shared/" + area + "/" + viewName;
}
You may be wondering why I don’t pass in the new identifying strategy to the factory and that’s because of a limitation in the way IControllerIdentificationStrategy works. This interface doesn’t allow you to consider RouteData when generating the service names. This leave us with having to subclass the factory. In the future it would be nice to see a more configurable way to discover controller names.
ASP.NET MVC and autofac amaze me at every turn with their extensibility.
