Usando JSON.NET como o serializador JSON padrão na ASP.NET MVC 3 - é possível?

101

É possível usar JSON.NET como serializador JSON padrão na ASP.NET MVC 3?

De acordo com minha pesquisa, parece que a única maneira de conseguir isso é estender o ActionResult, já que JsonResult em MVC3 não é virtual ...

Eu esperava que com a ASP.NET MVC 3 houvesse uma maneira de especificar um provedor plugável para serializar em JSON.

Pensamentos?

zam6ak
fonte
related: stackoverflow.com/questions/6883204/…
Ruben Bartelink

Respostas:

106

Acredito que a melhor maneira de fazer isso é - conforme descrito em seus links - estender ActionResult ou estender JsonResult diretamente.

Já para o método JsonResult que não é virtual no controlador isso não é verdade, basta escolher a sobrecarga certa. Isso funciona bem:

protected override JsonResult Json(object data, string contentType, Encoding contentEncoding)

EDIT 1 : Uma extensão JsonResult ...

public class JsonNetResult : JsonResult
{
    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        var response = context.HttpContext.Response;

        response.ContentType = !String.IsNullOrEmpty(ContentType) 
            ? ContentType 
            : "application/json";

        if (ContentEncoding != null)
            response.ContentEncoding = ContentEncoding;

        // If you need special handling, you can call another form of SerializeObject below
        var serializedObject = JsonConvert.SerializeObject(Data, Formatting.Indented);
        response.Write(serializedObject);
    }

EDIT 2 : Eu removi a seleção de Dados sendo nulos conforme as sugestões abaixo. Isso deve deixar as versões mais recentes do JQuery felizes e parece a coisa mais sensata a se fazer, já que a resposta pode ser desserializada incondicionalmente. Porém, esteja ciente de que esse não é o comportamento padrão para respostas JSON da ASP.NET MVC, que em vez disso responde com uma string vazia, quando não há dados.

Asgerhallas
fonte
1
O código se refere a MySpecialContractResolver, que não está definido. Esta questão ajuda com isso (e estava muito relacionada ao problema que eu tinha que resolver): stackoverflow.com/questions/6700053/…
Elliveny
1
Obrigado pela ótima resposta. Por que o if (Data == null) retorna; ? Para o meu caso de uso, eu queria voltar qualquer que fosse o padrão JSON, que Json.Net faz fielmente, mesmo para nulo (retornando "nulo"). Ao interceptar valores nulos, você acaba enviando a string vazia de volta para eles, o que desvia do padrão e causa problemas de downstream - por exemplo, com jQuery 1.9.1: stackoverflow.com/a/15939945/176877
Chris Moschini
1
@Chris Moschini: Você está absolutamente certo. É errado retornar uma string vazia. Mas ele deve retornar o valor json nulo ou um objeto json vazio então? Também não tenho certeza se retornar um valor onde um objeto é esperado é livre de problemas. Mas, de qualquer forma, o código atual não é bom nesse aspecto.
asgerhallas
1
Há um bug no Json.Net que faz com que o IE9 e versões anteriores falhem ao analisar as datas ISO 8601 que a Json.Net produz. A correção para isso está incluída nesta resposta: stackoverflow.com/a/15939945/176877
Chris Moschini
1
@asgerhallas, @Chris Moschini E sobre a verificação JsonResult do asp.net mvc padrão if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException(MvcResources.JsonRequest_GetNotAllowed);? Acho que preciso adicionar esta verificação na resposta (sem interno, MvcResources.JsonRequest_GetNotAllowedmas com alguma mensagem personalizada). Além disso, que tal 2 outras verificações de mvc asp.net padrão - MaxJsonLength e RecursionLimit? Precisamos deles se usarmos json.net?
chromigo
60

Implementei isso sem a necessidade de um controlador de base ou injeção.

Usei filtros de ação para substituir o JsonResult por um JsonNetResult.

public class JsonHandlerAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
       var jsonResult = filterContext.Result as JsonResult;

        if (jsonResult != null)
        {
            filterContext.Result = new JsonNetResult
            {
                ContentEncoding = jsonResult.ContentEncoding,
                ContentType = jsonResult.ContentType,
                Data = jsonResult.Data,
                JsonRequestBehavior = jsonResult.JsonRequestBehavior
            };
        }

        base.OnActionExecuted(filterContext);
    }
}

No Global.asax.cs Application_Start (), você precisaria adicionar:

GlobalFilters.Filters.Add(new JsonHandlerAttribute());

Para fins de conclusão, aqui está minha classe de extensão JsonNetResult que peguei em outro lugar e que modifiquei um pouco para obter o suporte de vaporização correto:

public class JsonNetResult : JsonResult
{
    public JsonNetResult()
    {
        Settings = new JsonSerializerSettings
        {
            ReferenceLoopHandling = ReferenceLoopHandling.Error
        };
    }

    public JsonSerializerSettings Settings { get; private set; }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");
        if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
            throw new InvalidOperationException("JSON GET is not allowed");

        HttpResponseBase response = context.HttpContext.Response;
        response.ContentType = string.IsNullOrEmpty(this.ContentType) ? "application/json" : this.ContentType;

        if (this.ContentEncoding != null)
            response.ContentEncoding = this.ContentEncoding;
        if (this.Data == null)
            return;

        var scriptSerializer = JsonSerializer.Create(this.Settings);
        scriptSerializer.Serialize(response.Output, this.Data);
    }
}
MDB
fonte
1
Esta é uma boa solução. Faz com que o nativo return Json()use Json.Net.
OneHoopyFrood
1
Para quem está se perguntando como isso funciona, ele intercepta o JsonResultde Json()e o converte em um JsonNetResult. Ele faz isso usando a aspalavra - chave que retorna null se a conversão não for possível. Muito bacana. 10 pontos para a Grifinória!
OneHoopyFrood
4
Porém, a questão é: o serializador padrão é executado no objeto antes de ser interceptado?
OneHoopyFrood
Esta é uma resposta fantástica - com a maior flexibilidade. Como meu projeto já estava fazendo todos os tipos de soluções manuais no front end, não pude adicionar um filtro global - isso exigiria uma mudança maior. Acabei resolvendo o problema apenas nas ações do controlador quando necessário usando o atributo nas ações do meu controlador. No entanto, chamei-o de - [BetterJsonHandler]:-).
Simcha Khabinsky
retornando this.Json (nulo); ainda não retorna nada
Brunis
27

Use o conversor JSON da Newtonsoft:

public ActionResult DoSomething()
{
    dynamic cResponse = new ExpandoObject();
    cResponse.Property1 = "value1";
    cResponse.Property2 = "value2";
    return Content(JsonConvert.SerializeObject(cResponse), "application/json");
}
Sami Beyoglu
fonte
7
Não tenho certeza se isso é hacky ou não, mas puta merda é mais fácil do que criar classes de extensão, apenas para retornar uma string json estúpida.
dennis.sheppard
21

Eu sei que isso é bem depois que a pergunta foi respondida, mas estou usando uma abordagem diferente, pois estou usando injeção de dependência para instanciar meus controladores.

Substituí o IActionInvoker (injetando a propriedade ControllerActionInvoker do controlador) por uma versão que substitui o método InvokeActionMethod.

Isso significa que não há alteração na herança do controlador e pode ser facilmente removido quando eu atualizar para MVC4, alterando o registro do contêiner DI para TODOS os controladores

public class JsonNetActionInvoker : ControllerActionInvoker
{
    protected override ActionResult InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
    {
        ActionResult invokeActionMethod = base.InvokeActionMethod(controllerContext, actionDescriptor, parameters);

        if ( invokeActionMethod.GetType() == typeof(JsonResult) )
        {
            return new JsonNetResult(invokeActionMethod as JsonResult);
        }

        return invokeActionMethod;
    }

    private class JsonNetResult : JsonResult
    {
        public JsonNetResult()
        {
            this.ContentType = "application/json";
        }

        public JsonNetResult( JsonResult existing )
        {
            this.ContentEncoding = existing.ContentEncoding;
            this.ContentType = !string.IsNullOrWhiteSpace(existing.ContentType) ? existing.ContentType : "application/json";
            this.Data = existing.Data;
            this.JsonRequestBehavior = existing.JsonRequestBehavior;
        }

        public override void ExecuteResult(ControllerContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }
            if ((this.JsonRequestBehavior == JsonRequestBehavior.DenyGet) && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
            {
                base.ExecuteResult(context);                            // Delegate back to allow the default exception to be thrown
            }

            HttpResponseBase response = context.HttpContext.Response;
            response.ContentType = this.ContentType;

            if (this.ContentEncoding != null)
            {
                response.ContentEncoding = this.ContentEncoding;
            }

            if (this.Data != null)
            {
                // Replace with your favourite serializer.  
                new Newtonsoft.Json.JsonSerializer().Serialize( response.Output, this.Data );
            }
        }
    }
}

--- EDITAR - Atualizado para mostrar o registro do contêiner para controladores. Estou usando o Unity aqui.

private void RegisterAllControllers(List<Type> exportedTypes)
{
    this.rootContainer.RegisterType<IActionInvoker, JsonNetActionInvoker>();
    Func<Type, bool> isIController = typeof(IController).IsAssignableFrom;
    Func<Type, bool> isIHttpController = typeof(IHttpController).IsAssignableFrom;

    foreach (Type controllerType in exportedTypes.Where(isIController))
    {
        this.rootContainer.RegisterType(
            typeof(IController),
            controllerType, 
            controllerType.Name.Replace("Controller", string.Empty),
            new InjectionProperty("ActionInvoker")
        );
    }

    foreach (Type controllerType in exportedTypes.Where(isIHttpController))
    {
        this.rootContainer.RegisterType(typeof(IHttpController), controllerType, controllerType.Name);
    }
}

public class UnityControllerFactory : System.Web.Mvc.IControllerFactory, System.Web.Http.Dispatcher.IHttpControllerActivator
{
    readonly IUnityContainer container;

    public UnityControllerFactory(IUnityContainer container)
    {
        this.container = container;
    }

    IController System.Web.Mvc.IControllerFactory.CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
    {
        return this.container.Resolve<IController>(controllerName);
    }

    SessionStateBehavior System.Web.Mvc.IControllerFactory.GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
    {
        return SessionStateBehavior.Required;
    }

    void System.Web.Mvc.IControllerFactory.ReleaseController(IController controller)
    {
    }

    IHttpController IHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)
    {
        return this.container.Resolve<IHttpController>(controllerType.Name);
    }
}
Robert Slaney
fonte
Legal, mas como você usa isso? Ou melhor, como você injetou?
Adaptabi
1 para usar a forma Stream de .Serialize (). Eu ia apontar que você pode apenas usar JsonConvert como a outra resposta principal, mas sua abordagem gradualmente expande objetos longos / grandes - isso é um aumento de desempenho gratuito, especialmente se o cliente downstream puder lidar com respostas parciais.
Chris Moschini,
1
boa implementação. Esta deve ser a resposta!
Kat Lim Ruiz,
Bom trabalho, essa era a única coisa para a qual eu estava usando um controlador de base.
Chris Diver
realmente bom - isso é muito melhor do que substituir a função Json (), já que em cada local onde você retornará um JsonResult, isso entrará em ação e fará sua mágica. Para aqueles que não usam DI, basta adicionar a substituição protegida IActionInvoker CreateActionInvoker () {return new JsonNetActionInvoker ();} ao seu controlador de base
Avi Pinto
13

Expandindo a resposta de https://stackoverflow.com/users/183056/sami-beyoglu , se você definir o tipo de conteúdo, o jQuery será capaz de converter os dados retornados em um objeto para você.

public ActionResult DoSomething()
{
    dynamic cResponse = new ExpandoObject();
    cResponse.Property1 = "value1";
    cResponse.Property2 = "value2";
    return Content(JsonConvert.SerializeObject(cResponse), "application/json");
}
StokeStoke
fonte
Obrigado, eu tenho um mix híbrido e essa é a única coisa que funcionaria para mim.
done_merson 01 de
Eu usei isso com JSON.NET assim: JObject jo = GetJSON(); return Content(jo.ToString(), "application/json");
John Mott
6

Minha postagem pode ajudar alguém.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
namespace MultipleSubmit.Service
{
    public abstract class BaseController : Controller
    {
        protected override JsonResult Json(object data, string contentType,
            Encoding contentEncoding, JsonRequestBehavior behavior)
        {
            return new JsonNetResult
            {
                Data = data,
                ContentType = contentType,
                ContentEncoding = contentEncoding,
                JsonRequestBehavior = behavior
            };
        }
    }
}


using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MultipleSubmit.Service
{
    public class JsonNetResult : JsonResult
    {
        public JsonNetResult()
        {
            Settings = new JsonSerializerSettings
            {
                ReferenceLoopHandling = ReferenceLoopHandling.Error
            };
        }
        public JsonSerializerSettings Settings { get; private set; }
        public override void ExecuteResult(ControllerContext context)
        {
            if (context == null)
                throw new ArgumentNullException("context");
            if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && string.Equals
(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
                throw new InvalidOperationException("JSON GET is not allowed");
            HttpResponseBase response = context.HttpContext.Response;
            response.ContentType = string.IsNullOrEmpty(this.ContentType) ? 
"application/json" : this.ContentType;
            if (this.ContentEncoding != null)
                response.ContentEncoding = this.ContentEncoding;
            if (this.Data == null)
                return;
            var scriptSerializer = JsonSerializer.Create(this.Settings);
            using (var sw = new StringWriter())
            {
                scriptSerializer.Serialize(sw, this.Data);
                response.Write(sw.ToString());
            }
        }
    }
} 

public class MultipleSubmitController : BaseController
{
   public JsonResult Index()
    {
      var data = obj1;  // obj1 contains the Json data
      return Json(data, JsonRequestBehavior.AllowGet);
    }
}    
Névoa
fonte
Eu estava procurando uma solução real e você era a única resposta correta
Richard Aguirre
Obrigado. Tendo já implementado o meu próprio BaseController, esta foi a mudança de menor impacto - só tive que adicionar a classe e atualizar BaseController.
AndrewP
4

Fiz uma versão que torna as ações de serviço da web seguras e simples. Você o usa assim:

public JsonResult<MyDataContract> MyAction()
{
    return new MyDataContract();
}

A classe:

public class JsonResult<T> : JsonResult
{
    public JsonResult(T data)
    {
        Data = data;
        JsonRequestBehavior = JsonRequestBehavior.AllowGet;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        // Use Json.Net rather than the default JavaScriptSerializer because it's faster and better

        if (context == null)
            throw new ArgumentNullException("context");

        var response = context.HttpContext.Response;

        response.ContentType = !String.IsNullOrEmpty(ContentType)
            ? ContentType
            : "application/json";

        if (ContentEncoding != null)
            response.ContentEncoding = ContentEncoding;

        var serializedObject = JsonConvert.SerializeObject(Data, Formatting.Indented);
        response.Write(serializedObject);
    }

    public static implicit operator JsonResult<T>(T d)
    {
        return new JsonResult<T>(d);
    }
}
Curtis Yallop
fonte
mas por que você deseja ter um tipo forte de JsonResult? : D você perde os resultados dos tipos anônimos e não ganha nada do lado do cliente, já que não está usando classes C # de qualquer maneira?
mikus de
1
@mikus É seguro para tipos no lado do servidor: o método deve retornar o tipo MyDataContract. Deixa claro para o lado do cliente exatamente qual estrutura de dados está sendo retornada. Também é conciso e legível - JsonResult <T> converte automaticamente qualquer tipo que está sendo retornado ao Json e você não precisa fazer nada.
Curtis Yallop