Monday, December 14, 2009 8:03 AM
Kazi Manzur Rashid
Meet my new ASP.NET MVC Extension - System.Web.Mvc.Extensibility
After hearing Phil Haack and Scott Hanselman in their latest podcast and PDC session I decided to give a good look at the MvcTurbine project in this weekend. I totally agree the with intend of this project, let your favorite IoC to rule everywhere, but I do not think it has been implemented in the correct way. There are three important design decisions that the MvcTurbine team should re-evaluate:
Generic Component Registration API
Rather than allowing you to use your preferred IoC registration features, it prefers to use its own generic syntax for component registration, which is simply wrong. In real application, we need more support from our IoC such as lifetime management, auto wiring, modularity like (Ninject/Autfac Modules, StructureMap Registry) etc. In case of this generic API we have to throw away the features of these IoCs.
Missing Common Service Locator
I think the Common Service Locator(CSL) has become the standard to abstract the underlying IoC from your application code and all the popular IoCs in .NET world has an adapter for CSL. But rather than using/extending it, it has its own version of service locator.
Limited support for Action Filter injection
It uses a host kinda Attribute to inject the dependencies into the actual action filter attribute and this host is not capable of passing the attribute specific values to the actual attribute and even if this feature is implemented in the future it will be based upon the string based property name which would work on top of reflection.
Now, lets see how my new extension solves the above issues and how easily you can start plugging it in your application. First lets take a quick look of the project structure of this extension.
As you can see other than the Core, each IoC has its own implementation. Currently it has the support for Autofac, Ninject, StructureMap, Unity and Windsor. When plugging it in your application, it requires five simple steps:
- Add reference of the Core Project.
- Add reference of you prefered IoC project and binaries of that IoC.
- Change the Global.asax so that it inherits from your prefered IoC supported
HttpApplication. - Create a class that inherits from
RegisterRoutesBase and register your routes over there. - Create a class as per your IoC that allows component registration. For example, both Autofac and Ninject supports Module for component registration and StructureMap uses Registry. Since both Unity and Windsor does not have modules support there is an interface
IModule which you have to implement for your application component registration (Check the sample application for the complete reference).
And that’s it, you are all set to go.
Now, lets see the above steps in more detail, I will use the Autofac in this case. But you can check the other examples if your prefered IoC is other than Autofac.
First, lets add the required references, once your are done, your Mvc application will have the following references:
Next, change the global.asax so that it inherits from the Autofac supported HttpApplication like the following:
namespace Demo.Web.Autofac
{
using System.Web.Mvc.Extensibility.Autofac;
public class MvcApplication : AutofacMvcApplication
{
}
}
Next, create a class to register the routes:
public class RegisterRoutes : RegisterRoutesBase
{
public RegisterRoutes(RouteCollection routes) : base(routes)
{
}
protected override void ExecuteCore(IServiceLocator locator)
{
Routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
Routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Product", action = "Index", id = string.Empty } );
}
}
And at last an Autofac module to register your application specific components:
public class RegisterServices : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.Register<InMemoryDatabasae>().As<IDatabase>();
builder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>));
}
}
If you browse the sample applications, you will find there are quite a few things that is going under the hood. Lets take a quick look how the Product Controller is implemented:
public class ProductController : Controller
{
private readonly IRepository<Product> repository;
public ProductController(IRepository<Product> repository)
{
this.repository = repository;
}
public ActionResult Index()
{
return View(repository.All().Select(product => product.AsDisplayModel()));
}
public ActionResult Details(int id)
{
return View(repository.Get(id).AsDisplayModel());
}
public ActionResult Create()
{
return View(new ProductEditModel());
}
[HttpPost]
public ActionResult Create(FormCollection form)
{
ProductEditModel model = new ProductEditModel();
if (TryUpdateModel(model, form.ToValueProvider()))
{
Product product = model.AsProduct();
product.Id = repository.All().LastOrDefault().Id + 1;
repository.Add(product);
return RedirectToAction("Index");
}
return View(model);
}
public ActionResult Edit(int id)
{
return View(repository.Get(id).AsEditModel());
}
[HttpPost]
public ActionResult Edit(ProductEditModel model)
{
if (ModelState.IsValid)
{
Product product = model.AsProduct();
repository.Update(product);
return RedirectToAction("Index");
}
return View(model);
}
public ActionResult Delete(int id)
{
return View(repository.Get(id).AsDisplayModel());
}
[HttpPost]
public ActionResult Delete(int id, string confirm)
{
repository.Delete(id);
return RedirectToAction("Index");
}
}
As you can see the the controller requires the repository which we have registered in the RegisterServices module, but the controller, action filters (Not decorated in the controller) and model binders are automatically registered by this extension.
Lets take a look at the Model Binder, in the sample application we have only one Model Binder which is used to create the product edit view model. Under the hood only the category id and supplier Id is passed and it uses the corresponding repositories to populate the Category and Supplier property of this view model.
[BindingTypes(typeof(ProductEditModel))]
public class ProductEditModelBinder : DefaultModelBinder
{
private readonly IRepository<Category> categoryRepository;
private readonly IRepository<Supplier> supplierRepository;
public ProductEditModelBinder(IRepository<Category> categoryRepository, IRepository<Supplier> supplierRepository)
{
this.categoryRepository = categoryRepository;
this.supplierRepository = supplierRepository;
}
protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
const string CategoryProperty = "Category";
const string SupplierProperty = "Supplier";
if (propertyDescriptor.Name.Equals(CategoryProperty, StringComparison.OrdinalIgnoreCase))
{
return GetValue(CategoryProperty, categoryRepository, controllerContext, bindingContext);
}
if (propertyDescriptor.Name.Equals(SupplierProperty, StringComparison.OrdinalIgnoreCase))
{
return GetValue(SupplierProperty, supplierRepository, controllerContext, bindingContext);
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
private static object GetValue<TEntity>(string propertyName, IRepository<TEntity> repository, ControllerContext controllerContext, ModelBindingContext bindingContext) where TEntity : EntityBase
{
ValueProviderResult result = bindingContext.ValueProvider.GetValue(controllerContext, propertyName);
int? id = (result != null) ? (int?) result.ConvertTo(typeof(int?)) : null;
return (id.HasValue && id.Value > 0) ? repository.Get(id.Value) : default(TEntity);
}
}
The model binder inherits from the default model binder so that we can advantages of built-in features, the only exception is the Category and Supplier property where we are using the repository to retrieve it from our data store. One important thing I think you noticed that the Model Binder is decorated with an special attribute BindingTypes, this attribute indicates for which type(s) the binder will be used. This extension uses this attribute to register the model binders in the Model Binder collection.
And at last the Filters, the following is the code of PopulateCategoriesAttriute action filter:
public sealed class PopulateCategoriesAttribute : PopulateModelAttribute
{
private readonly IRepository<Category> repository;
public PopulateCategoriesAttribute(IRepository<Category> repository)
{
this.repository = repository;
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
ProductEditModel editModel = filterContext.Controller.ViewData.Model as ProductEditModel;
if (editModel != null)
{
editModel.Categories = new SelectList(repository.All(), "Id", "Name", editModel.Category);
}
}
}
As we can see that the Filter accept a repository in the constructor and it has not been decorated in the Controller methods, if you are wondering then how this filter come into action, let me explain a bit. When Oxite was initially released a lot people criticized it, but in next (current) version the Oxite came up with lots of excellent ideas. The Filter registration that I about to show you is taken from the Oxite with a slight change in the implementation. Although, this extension support filter property injection, but I would suggest to use the following fluent registration. To apply filter in controller, you first have to create a class which inherits from RegisterFiltersBase class. The following shows how the PopulateCategoriesAttribute and PopulateSuppliersAttribute are applied in the product controller of the sample application.
public class RegisterFilters : RegisterFiltersBase
{
public RegisterFilters(IFilterRegistry filterRegistry) : base(filterRegistry)
{
}
protected override void ExecuteCore(IServiceLocator serviceLocator)
{
FilterRegistry.Register<ProductController, PopulateCategoriesAttribute, PopulateSuppliersAttribute>(c => c.Create())
.Register<ProductController, PopulateCategoriesAttribute, PopulateSuppliersAttribute>(c => c.Create(null))
.Register<ProductController, PopulateCategoriesAttribute, PopulateSuppliersAttribute>(c => c.Edit(0))
.Register<ProductController, PopulateCategoriesAttribute, PopulateSuppliersAttribute>(c => c.Edit(null));
}
}
There are quite a few overloads for the Filter registration, if you want to apply filters to the whole controller, just skip the action part.
And that’s it for today.
I also think this would be nice addition for the MVCContrib project. So please download the code and play with it and do let me know your feedbacks.
Source Code: github
Filed under: Asp.net, MVC, ASPNETMVC, ASP.NET MVC, Common Service Locator, IoC/DI, Unity, Open Source, Ninject, Autofac, Windsor