2016-06-30 38 views
9

Próbuję napisać filtr, który opakowuje dane zgodnie z JSON API spec i do tej pory mam to działa we wszystkich przypadkach, w których bezpośrednio zwraca ActionResult, takich jak ComplexTypeJSON. Próbuję go uruchomić w sytuacjach takich jak ComplexType, gdzie nie muszę ciągle uruchamiać funkcji Json.Czy możliwe jest przechwycenie działania, które staje się ContentResult?

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

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

Jednak do czasu public override void OnActionExecuted(ActionExecutedContext filterContext) biegnie kiedy przejdź do ComplexType The filterContext.Result jest zawartość Wynik, który jest po prostu ciągiem gdzie filterContext.Result.Content jest prosta:

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

Czy istnieje sposób mogę ustawić coś, aby ComplexType stał się JsonResult zamiast ContentResult?

Dla kontekście, oto dokładne pliki:

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

Czy możesz pokazać więcej swojego kodu? Zapewnić nam więcej kontekstu? Wyjaśnij więcej terminów tym, którzy nie znają Twojego API JSON? – antiduh

+0

Nic z tego nie wynika z mojego interfejsu API JSON, ale podałem dokładne pliki filtra i kontrolera testowego. –

+0

, więc nie można uzyskać dostępu do listy w metodzie "ComposeResultModel". Czy mogę poprawić? –

Odpowiedz

3

Istnieją dwie opcje:

1.Wykorzystanie ApiController zamiast Controller

apicontroller zwróci wynik json, a domyślne serializer jest Newtonsoft.json(here), dzięki czemu można używać jak ten poniżej:

//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 swoją odpowiedź z własną ActionResult jeśli upierasz się przy użyciu 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" }); 
} 

, a jeśli chcesz uzyskać ciąg reakcji z filtrem do dziennika lub coś:

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

Tak, tego rodzaju żądanie powinno być obsługiwane przez interfejs API. Ale co jest złego w wysyłaniu wyniku OOB JSON 'result' –

+0

Ostatecznie połączenie niestandardowego wyniku działania i mojego atrybutu filtru doprowadziło do pożądanego zachowania. Dzięki za wyjaśnienie, było świetnie! –

3

myślę, że to jest to, czego szukasz:

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

Od @roosteronacid: return jsonresult in actionfilter

0

Właśnie napotkałem ten sam problem i znalazłem nieco inne podejście. Podstawowy pomysł był z NOtherDev. Chciałbym wprowadzić 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); 
    } 
} 

W tym przypadku każde żądanie zaczynające się od „/ api /” będzie zmieniły wynik json, jednak tylko wtedy, gdy actionReturnValue nie jest typem odziedziczone już ActionResult.

IActionInvoker jest rozwiązywany przez DependencyResolver, więc musisz zdefiniować rejestrację w swoim ulubionym pojemniku ioc, który ustawiłeś jako DependencyResolver.

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

Dla JsonResult można użyć wbudowanego lub this.

W przypadku, gdy używasz asyncowych metod działania, powinieneś dziedziczyć po AsyncControllerActionInvoker zamiast ControllerActionInvoker i zakładam, że będziesz musiał dodać kolejną rejestrację również dla IAsyncActionInvoker. Nie jestem pewien zmian w asynchronicznej części samego wywoływacza.