2016-06-30 21 views
9

Sto tentando di scrivere un filtro che avvolge i dati per seguire il JSON API spec e finora ho funzionato su tutti i casi in cui restituisco direttamente un ActionResult, come ad esempio ComplexTypeJSON. Sto cercando di farlo funzionare in situazioni come ComplexType dove non devo eseguire costantemente la funzione Json. ciÈ possibile intercettare un'azione da diventare un ContentResult?

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

è un senso che posso:

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

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

Tuttavia, per il momento in public override void OnActionExecuted(ActionExecutedContext filterContext) viene eseguito quando ci si dirige verso ComplexType, il filterContext.Result è un risultato Content, che è solo una stringa in cui filterContext.Result.Content è semplicemente impostare qualcosa per rendere ComplexType diventa JsonResult anziché ContentResult?

per il contesto, qui i file esatto:

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; 
     } 
    } 
} 
+0

Puoi mostrare altro codice? Forniscici più contesto? Spiegare più termini a coloro che non hanno familiarità con la tua API JSON? – antiduh

+0

Nulla di ciò è specifico per la mia API JSON, ma ho fornito i file esatti per il filtro e il controller di test. –

+0

quindi, non sei in grado di accedere alla tua lista nel metodo 'ComposeResultModel'. Mi correggo? –

risposta

3

Ci sono due opzioni:

1.Use ApiController anziché controller

L'apicontroller tornerà risultato JSON, e il serializzatore predefinito è Newtonsoft.json(here), in modo da poter utilizzare come questo qui sotto:

//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 la vostra risposta con il proprio ActionResult se insistete usando 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" }); 
} 

e se si desidera ottenere stringa di risposta da filtro per log o qualcosa:

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

sì, questo tipo di richiesta dovrebbe essere gestito da un'API. Ma cosa c'è di sbagliato nel rinviare il risultato OOB JSON ' –

+0

Alla fine la combinazione del action result personalizzato e dell'attributo del mio filtro ha portato al comportamento desiderato. Grazie per la spiegazione, è stato fantastico! –

3

Credo che questo è ciò che stai cercando:

public class JSONAPIFilterAttribute : ActionFilterAttribute, IActionFilter 
{ 
    void IActionFilter.OnActionExecuted(ActionExecutedContext context) 
    { 
     context.Result = new JsonResult 
     { 
      Data = ((ViewResult)context.Result).ViewData.Model 
     }; 
    } 
} 

Da @roosteronacid: return jsonresult in actionfilter

0

Ho appena riscontrato lo stesso problema e ho trovato un approccio leggermente diverso. L'idea di base era da NOtherDev. Vorrei introdurre un IActionInvoker.

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

    protected override ActionResult CreateActionResult(ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue) 
    { 
     if (controllerContext.HttpContext.Request.Path.StartsWith("/api/")) 
     { 
      return (actionReturnValue as ActionResult) 
       ?? new JsonResult 
       { 
        Data = actionReturnValue, 
        JsonRequestBehavior = JsonRequestBehavior.AllowGet 
       }; 
     } 
     return base.CreateActionResult(controllerContext, actionDescriptor, actionReturnValue); 
    } 
} 

In questo caso ogni richiesta inizia con "/ api /" avranno trasformato risultato a json, ma solo quando il actionReturnValue non è un tipo ereditato da ActionResult già.

IActionInvoker è stato risolto da DependencyResolver, quindi è necessario definire la registrazione nel contenitore di ioc preferito che è stato impostato come DependencyResolver.

myFavoriteContainer.Register<IActionInvoker, ControllerActionInvokerWithDefaultJsonResult>(Lifestyle.Transient); 

Per JsonResult è possibile utilizzare il built-in o this.

In un caso si stanno utilizzando metodi di azione asincrona, si dovrebbe ereditare da AsyncControllerActionInvoker anziché ControllerActionInvoker e presumo che sarà necessario aggiungere un'altra registrazione per IAsyncActionInvoker. Non sono sicuro dei cambiamenti nella parte asincrona dell'invoker stesso.