Saturday, September 22, 2012

Non-ActionResult action return type in ASP.NET MVC

In ASP.NET MVC, there's quite silly behavior when the controller's action method returns type that is not ActionResult-derived. The default ActionInvoker, which is responsible for invoking action code and interpret its result, checks if the returned instance is ActionResult and if not, returns plain string representation of the object (type name by default):

protected virtual ActionResult CreateActionResult(
    ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue)
{
    if (actionReturnValue == null)
    {
        return new EmptyResult();
    }

    ActionResult actionResult = (actionReturnValue as ActionResult) ??
        new ContentResult { Content = Convert.ToString(actionReturnValue, CultureInfo.InvariantCulture) };
    return actionResult;
}

I can see no real-life scenario in which ToString result returned as plain content can be useful. This in practice means that in ASP.NET MVC we're forced to use ActionResult or its derived types. This is especially annoying when you want your action method to be defined in an interface or used somewhere as a delegate.

The issue was solved much better in ASP.NET Web API - the actions in Web API controllers by design return POCO objects that are serialized correctly before sending it to the wire depending on the request and configuration - as XML, JSON etc.

To achieve similiar result in "normal" MVC controllers, let's replace the default ControllerActionInvoker right after creating the controller - in ControllerFactory - with our derived implementation that just overrides the virtual CreateActionResult method:

public class MyControllerFactory : DefaultControllerFactory
{
    public override IController CreateController(RequestContext context, string controllerName)
    {
        var controller = base.CreateController(context, controllerName);
        return ReplaceActionInvoker(controller);
    }

    private IController ReplaceActionInvoker(IController controller)
    {
        var mvcController = controller as Controller;
        if (mvcController != null)
            mvcController.ActionInvoker = new ControllerActionInvokerWithDefaultJsonResult();
        return controller;
    }
}

public class ControllerActionInvokerWithDefaultJsonResult : ControllerActionInvoker
{
    public const string JsonContentType = "application/json";

    protected override ActionResult CreateActionResult(
        ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue)
    {
        if (actionReturnValue == null)
            return new EmptyResult();

        return (actionReturnValue as ActionResult) ?? new ContentResult()
        {
            ContentType = JsonContentType,
            Content = JsonConvert.SerializeObject(actionReturnValue)
        };
    }
}

This simple implementation just serializes the returned objects to JSON, but it's easy to implement something more sophisticated here, like content negotiation patterns like Web API has. Feel free to use it and extend it if you find it useful - I've published it as a Gist for your convenience.

No comments:

Post a Comment