Phil Barresi Phil Barresi - 4 months ago 23
ASP.NET (C#) Question

Is it possible to intercept an action from becoming a ContentResult?

I am attempting to write a filter that wraps data to follow the JSON API spec and so far I've got it working on all cases where I directly return an ActionResult, such as

ComplexTypeJSON
. I am trying to get it to work in situations like
ComplexType
where I do not have to run the
Json
function constantly.

[JSONAPIFilter]
public IEnumerable<string> ComplexType()
{
return new List<string>() { "hello", "world" };
}

[JSONAPIFilter]
public JsonResult ComplexTypeJSON()
{
return Json(new List<string>() { "hello", "world" });
}


However, by the time
public override void OnActionExecuted(ActionExecutedContext filterContext)
runs when I navigate to
ComplexType
, the
filterContext.Result
is a Content Result, that is just a string where
filterContext.Result.Content
is simply:

"System.Collections.Generic.List`1[System.String]"


Is there a way I can set something up to make
ComplexType
become
JsonResult
rather than
ContentResult
?

For context, here are the exact files:

TestController.cs

namespace MyProject.Controllers
{
using System;
using System.Collections.Generic;
using System.Web.Mvc;

using MyProject.Filters;

public class TestController : Controller
{
[JSONAPIFilter]
public IEnumerable<string> ComplexType()
{
return new List<string>() { "hello", "world" };
}

[JSONAPIFilter]
public JsonResult ComplexTypeJSON()
{
return Json(new List<string>() { "hello", "world" });
}

// GET: Test
[JSONAPIFilter]
public ActionResult Index()
{
return Json(new { foo = "bar", bizz = "buzz" });
}

[JSONAPIFilter]
public string SimpleType()
{
return "foo";
}

[JSONAPIFilter]
public ActionResult Throw()
{
throw new InvalidOperationException("Some issue");
}
}
}


JSONApiFilter.cs

namespace MyProject.Filters
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;

using MyProject.Exceptions;
using MyProject.Models.JSONAPI;

public class JSONAPIFilterAttribute : ActionFilterAttribute, IExceptionFilter
{
private static readonly ISet<Type> IgnoredTypes = new HashSet<Type>()
{
typeof(FileResult),
typeof(JavaScriptResult),
typeof(HttpStatusCodeResult),
typeof(EmptyResult),
typeof(RedirectResult),
typeof(ViewResultBase),
typeof(RedirectToRouteResult)
};

private static readonly Type JsonErrorType = typeof(ErrorModel);

private static readonly Type JsonModelType = typeof(ResultModel);

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}

if (IgnoredTypes.Any(x => x.IsInstanceOfType(filterContext.Result)))
{
base.OnActionExecuted(filterContext);
return;
}

var resultModel = ComposeResultModel(filterContext.Result);
var newJsonResult = new JsonResult()
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = resultModel
};

filterContext.Result = newJsonResult;
base.OnActionExecuted(filterContext);
}

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var modelState = filterContext.Controller.ViewData.ModelState;

if (modelState == null || modelState.IsValid)
{
base.OnActionExecuting(filterContext);
}
else
{
throw new ModelStateException("Errors in ModelState");
}
}

public virtual void OnException(ExceptionContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}

if (filterContext.Exception == null) return;

// Todo: if modelstate error, do not provide that message
// set status code to 404

var errors = new List<string>();

if (!(filterContext.Exception is ModelStateException))
{
errors.Add(filterContext.Exception.Message);
}

var modelState = filterContext.Controller.ViewData.ModelState;
var modelStateErrors = modelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage).ToList();
if (modelStateErrors.Any()) errors.AddRange(modelStateErrors);

var errorCode = (int)System.Net.HttpStatusCode.InternalServerError;
var errorModel = new ErrorModel()
{
status = errorCode.ToString(),
detail = filterContext.Exception.StackTrace,
errors = errors,
id = Guid.NewGuid(),
title = filterContext.Exception.GetType().ToString()
};
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
filterContext.HttpContext.Response.StatusCode = errorCode;

var newResult = new JsonResult() { Data = errorModel, JsonRequestBehavior = JsonRequestBehavior.AllowGet };

filterContext.Result = newResult;
}

private ResultModel ComposeResultModel(ActionResult actionResult)
{
var newModelData = new ResultModel() { };

var asContentResult = actionResult as ContentResult;
if (asContentResult != null)
{
newModelData.data = asContentResult.Content;
return newModelData;
}

var asJsonResult = actionResult as JsonResult;
if (asJsonResult == null) return newModelData;

var dataType = asJsonResult.Data.GetType();
if (dataType != JsonModelType)
{
newModelData.data = asJsonResult.Data;
}
else
{
newModelData = asJsonResult.Data as ResultModel;
}

return newModelData;
}
}
}

Answer

There are two options:

1.use ApiController instead of Controller

The apicontroller will return json result,and the default serializer is Newtonsoft.json(here),so you can use like this below:

//the response type
public class SimpleRes
{
    [JsonProperty(PropertyName = "result")]
    public string Result;      
}

//the controller
 public class TestController : ApiController
 {  
    [HttpGet] 
    [HttpPost]
    [JSONAPIFilter]
    public SimpleRes TestAction()
    {
        return new SimpleRes(){Result = "hello world!"};
    }
 }

2.wrap your response with your own ActionResult if you insist using Controller:

//json container
public class AjaxMessageContainer<T>
{        
    [JsonProperty(PropertyName = "result")]
    public T Result { set; get; }
}

//your own actionresult
public class AjaxResult<T> : ActionResult
{        
    private readonly T _result;                      

    public AjaxResult(T result)
    {          
        _result = result;           
    }

    public override void ExecuteResult(ControllerContext context)
    {
        context.HttpContext.Response.Clear();
        context.HttpContext.Response.ContentType = "application/json";
        var result = JsonConvert.SerializeObject(new AjaxMessageContainer<T>
        {               
            Result = _result,               
        });
        var bytes =
            new UTF8Encoding().GetBytes(result);
        context.HttpContext.Response.OutputStream.Write(bytes, 0, bytes.Length);           
    }
}

//your controller
[JSONAPIFilter]
public AjaxResult<List<String>> TestSimple()
{
    return AjaxResult<List<String>>(new List<string>() { "hello", "world" });
}

and if you wanna get response string from filter for log or something:

var result  = filterContext.Response.Content.ReadAsStringAsync();