When building web applications, you often encounter scenarios where the same controller endpoint needs to return different views depending on the incoming request data. A typical approach involves stacking multiple conditional statements within a single action method to determine which view to render.
Consider this common pattern:
[HttpPost]
public ActionResult GetNodeDetails(TreeNode node)
{
var nodeName = node.Name.ToLowerInvariant();
if (nodeName == "documents")
{
// Load document-related data
return View("Documents");
}
if (nodeName == "messages")
{
// Load message-related data
return View("Messages");
}
return View("Default");
}
While this works, it becomes unwieldy as the number of conditions grows. The ActionNameSelectorAttribute class in ASP.NET MVC provides a cleaner alternative. By creating a custom attribute that inherits from this base class, you can map specific parameter values directly to designated action methods.
The framework already uses this mechanism internally—to example, the built-in ActionNameAttribute extends ActionNameSelectorAttribute to enable multiple methods sharing the same URL endpoint.
Building a Custom Parameter-Based Action Selector
Here's an implementation that routes requests based on a query parameter value:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ParameterBasedActionSelector : ActionNameSelectorAttribute
{
public string TargetParameter { get; private set; }
public ParameterBasedActionSelector(string parameterValue)
{
if (string.IsNullOrWhiteSpace(parameterValue))
{
throw new ArgumentException("Parameter value cannot be null or empty", nameof(parameterValue));
}
this.TargetParameter = parameterValue;
}
public override bool IsValidName(ControllerContext controllerContext,
string actionName, MethodInfo methodInfo)
{
var incomingValue = controllerContext.HttpContext.Request["category"];
return string.Equals(this.TargetParameter, incomingValue,
StringComparison.OrdinalIgnoreCase);
}
}
Applying the Custom Selector to Actions
With this attribute in place, you can distribute your logic across multiple clean action methods:
[ActionName("GetCategoryData")]
[ParameterBasedActionSelector("documents")]
[HttpPost]
public ActionResult LoadDocuments(CategoryItem item)
{
// Retrieve document data
return View("DocumentList");
}
[ActionName("GetCategoryData")]
[ParameterBasedActionSelector("messages")]
[HttpPost]
public ActionResult LoadMessages(CategoryItem item)
{
// Retrieve message data
return View("MessageList");
}
[ActionName("GetCategoryData")]
[ParameterBasedActionSelector("media")]
[HttpPost]
public ActionResult LoadMedia(CategoryItem item)
{
// Retrieve media data
return View("MediaGallery");
}
When a POST request arrives at /Controller/GetCategoryData, the MVC framework examines each decorated action method. The selector compares the incoming category parameter against each method's target value, invoking the first match found.
Practical Applications
This pattern proves particularly valuable in several scenarios:
- Role-based view selection: Serving different views based on user permissions
- Conditional rendering: Returning mobile versus desktop views
- Feature toggles: Directing traffic to updated or experimental implementations
The key advantage is separation of concerns—each action method handles a specific scenario, making your controller easier to maintain and test. The routing logic lives in reusable attributes rather than embedded within action implementations.