Dynamic Actions in ASP.NET MVC
Custom controller factories and custom action invokers are among the many extension points in ASP.NET MVC. Subclassing DefaultControllerFactory and ControllerActionInvoker allows developers to augment the standard behavior of executing controller actions based on route data. While the convention of having actions be public methods on Controller classes is useful, there are times when it is redundant to have a number of methods that simply put data into ViewData and return the view. Another scenario impeding code reuse is when a number of controllers share a set common actions, but could not benefit from inheritance because those controllers already inherit from another controller base.
In Castle's Monorail, a DynamicAction is simply an implementation of the IDynamicAction interface, which has a single method Execute. Controllers have a DynamicActions dictionary.
public class ViewAction {
public void Execute(Controller controller) { ... };
}
The keys of the dictionary are the action names and the values are instances of a class that implements IDynamicAction.
public class HomeController : Controller {
public HomeController() {
DynamicActions["about"] = new ViewAction();
}
}
When the request to /Home/About is handled, the Controller will find the "about" action name and invoke the Execute method on the ViewAction instance. In this way, many named actions are able to share the same action code without having to create action methods.
Building similar functionality in ASP.NET MVC requires hooking into the controller factory, action invoker and creating a controller base class.
public class DynamicAction {
public string ActionName { get; set; }
public Func<ActionResult> Result { get; set; }
}
The DynamicAction class defines the name of the action and the ActionResult method to invoke when a dynamic action is requested.
public class DynamicActionControllerBase : Controller {
private IList<DynamicAction> _dynamicActions = new List<DynamicAction>();
public IList<DynamicAction> DynamicActions
{
get { return _dynamicActions; }
set { _dynamicActions = value; }
}
}
The DynamicActionControllerBase extends MVC's standard Controller class and adds a public List of DynamicActions. For Controllers to use dynamic actions, they will need to extend this base class.
public class HomeController : DynamicActionControllerBase {
...
}
By default, the ControllerActionInvoker will attempt to use the route data's action value to execute a controller method with the same name. Since dynamic actions won't have matching methods, a custom action invoker is needed. DynamicActionInvoker extends ControllerActionInvoker.
public class DynamicActionInvoker : ControllerActionInvoker {
public DynamicAction DynamicAction { get; set; }
...
}
DynamicActionInvoker has a DynamicAction instance, which has a reference to the actual dynamic action method to invoke. When the MVC framework calls DynamicActionInvoker's InvokeAction method, ControllerActionInvoker's InvokeActionResult will provide the actual execution of the dynamic action.
public override bool InvokeAction(ControllerContext controllerContext, string actionName) {
base.InvokeActionResult(controllerContext, DynamicAction.Result());
return true;
}
The DynamicActionControllerFactory takes care of wiring up the custom action invoker to the controller.
public class DynamicActionControllerFactory : DefaultControllerFactory {
public override IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName) {
...
}
}
In its CreateController method, DynamicControllerFactory will create the controller by using MVC's DefaultControllerFactory.
Controller controller = base.CreateController(requestContext, controllerName) as Controller;
If the controller is found to be a DynamicActionController instance, then that controller's DynamicAction list is search for the action in the route data. If a DynamicAction is found in the list, the controller's ActionInvoker is set to a new DynamicActionInvoker and the DynamicAction is assigned to the invoker.
if (controller is DynamicActionControllerBase) {
string action = requestContext.RouteData.Values["action"] as string;
DynamicAction dynamicAction = (controller as DynamicActionControllerBase)
.DynamicActions
.FirstOrDefault(d => d.ActionName.Equals(action, StringComparison.CurrentCultureIgnoreCase));
if (dynamicAction != null) {
controller.ActionInvoker = new DynamicActionInvoker() { DynamicAction = dynamicAction };
}
return controller;
}
In HomeController's constructor, DynamicActions are added to the DynamicAction List.
public HomeController() {
DynamicActions.Add(new DynamicAction() { ActionName = "About", Result = View });
DynamicActions.Add(new DynamicAction() { ActionName = "Contact", Result = View });
DynamicActions.Add(new DynamicAction() {
ActionName = "Ping", Result = () => { Response.Write("OK"); return new EmptyResult(); }
});
DynamicActions.Add(new DynamicAction() {
ActionName = "API", Result = () => Json(new { FirstName = "John", LastName = "Zablocki" })
});
}
The first and second dynamic actions simply use the standard MVC Controller's View method as their result. The API and Ping methods demonstrate how to use different types of action results, such as the Controller's Json method or EmptyResult.
Loading the sample project for this article and navigating to
http://localhost:49898/home/about or
http://localhost:49898/home/contact shows these dynamic actions in action.
Download Sample Project
References
Monorail's documentation on dynamic actions
Article Posted: Thursday, March 18, 2010