Używanie JSON.NET jako domyślnego serializatora JSON w ASP.NET MVC 3 - czy to możliwe?

101

Czy można używać JSON.NET jako domyślnego serializatora JSON w ASP.NET MVC 3?

Według moich badań wydaje się, że jedynym sposobem na osiągnięcie tego jest rozszerzenie ActionResult, ponieważ JsonResult w MVC3 nie jest wirtualny ...

Miałem nadzieję, że z ASP.NET MVC 3 będzie sposób na określenie podłączalnego dostawcy do serializacji do JSON.

Myśli?

zam6ak
źródło
related: stackoverflow.com/questions/6883204/…
Ruben Bartelink

Odpowiedzi:

106

Uważam, że najlepszym sposobem na to jest - jak opisano w twoich linkach - rozszerzenie ActionResult lub bezpośrednie rozszerzenie JsonResult.

Jeśli chodzi o metodę JsonResult, która nie jest wirtualna na kontrolerze, to nie jest prawda, po prostu wybierz odpowiednie przeciążenie. To działa dobrze:

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

EDYCJA 1 : rozszerzenie 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);
    }

EDYCJA 2 : Usunąłem sprawdzanie danych, które są zerowe, zgodnie z poniższymi sugestiami. Powinno to uszczęśliwić nowsze wersje JQuery i wydaje się rozsądne, ponieważ odpowiedź może zostać bezwarunkowo zdeserializowana. Należy jednak pamiętać, że nie jest to domyślne zachowanie dla odpowiedzi JSON z ASP.NET MVC, które raczej reagują pustym ciągiem, gdy nie ma danych.

asgerhallas
źródło
1
Kod odwołuje się do MySpecialContractResolver, który nie jest zdefiniowany. To pytanie pomaga w tym (i było bardzo związane z problemem, który musiałem rozwiązać): stackoverflow.com/questions/6700053/…
Elliveny
1
Dzięki za świetną odpowiedź. Dlaczego powrót if (Data == null); ? W moim przypadku chciałem odzyskać dowolny standard JSON, który wiernie robi Json.Net, nawet dla null (zwracając „null”). Przechwytując wartości null, w końcu wysyłasz pusty ciąg znaków z powrotem dla nich, który odbiega od standardu i powoduje problemy z dalszymi kanałami - na przykład z jQuery 1.9.1: stackoverflow.com/a/15939945/176877
Chris Moschini
1
@Chris Moschini: Masz całkowitą rację. Błędem jest zwracanie pustego ciągu. Ale czy w takim razie powinien zwrócić wartość json null, czy pusty obiekt json? Nie jestem pewien, czy zwrócenie wartości, w której oczekiwany jest obiekt, również nie powoduje problemów. Ale tak czy inaczej, obecny kod nie jest dobry pod tym względem.
asgerhallas
1
W Json.Net występuje błąd, który powoduje, że IE9 i starsze nie analizują danych ISO 8601 Dates tworzonych przez Json.Net. Rozwiązanie tego problemu jest zawarte w tej odpowiedzi: stackoverflow.com/a/15939945/176877
Chris Moschini,
1
@asgerhallas, @Chris Moschini A co z domyślnym sprawdzaniem JsonResult asp.net mvc if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException(MvcResources.JsonRequest_GetNotAllowed);? Myślę, że trzeba dodać to sprawdzenie w odpowiedzi (bez wewnętrznego, MvcResources.JsonRequest_GetNotAllowedale z jakimś niestandardowym komunikatem). A co z 2 innymi domyślnymi kontrolami mvc asp.net - MaxJsonLength i RecursionLimit? Czy potrzebujemy ich, jeśli używamy json.net?
chromigo
60

Zaimplementowałem to bez potrzeby bazowego kontrolera czy wtrysku.

Użyłem filtrów akcji, aby zamienić JsonResult na 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);
    }
}

W Global.asax.cs Application_Start () należałoby dodać:

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

Dla porządku, oto moja klasa rozszerzeń JsonNetResult, którą odebrałem z innego miejsca i którą nieznacznie zmodyfikowałem, aby uzyskać poprawną obsługę parowania:

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
źródło
1
To fajne rozwiązanie. Sprawia, że ​​natywny return Json()w efekcie używa Json.Net.
OneHoopyFrood
1
Dla każdego, kto zastanawia się, jak to działa, przechwytuje JsonResultfrom Json()i konwertuje go na JsonNetResult. Czyni to za pomocą assłowa kluczowego, które zwraca wartość null, jeśli konwersja nie jest możliwa. Bardzo fajnie. 10 punktów dla Gryffindoru!
OneHoopyFrood
4
Pytanie jednak, czy domyślny serializator działa na obiekcie przed jego przechwyceniem?
OneHoopyFrood
To fantastyczna odpowiedź - z największą elastycznością. Ponieważ mój projekt wykonywał już wszelkiego rodzaju ręczne rozwiązania na froncie, nie mogłem dodać globalnego filtra - wymagałoby to większej zmiany. Skończyło się na tym, że rozwiązałem problem tylko w akcjach kontrolera, gdy było to konieczne, używając atrybutu akcji mojego kontrolera. Jednak nazwałem to - [BetterJsonHandler]:-).
Simcha Khabinsky
zwracając this.Json (null); nadal nic nie zwraca
Brunis
27

Użyj konwertera JSON firmy Newtonsoft:

public ActionResult DoSomething()
{
    dynamic cResponse = new ExpandoObject();
    cResponse.Property1 = "value1";
    cResponse.Property2 = "value2";
    return Content(JsonConvert.SerializeObject(cResponse), "application/json");
}
Sami Beyoglu
źródło
7
Nie jestem pewien, czy to jest hacky, czy nie, ale święte bzdury są łatwiejsze niż tworzenie klas rozszerzeń, aby zwrócić głupi ciąg JSON.
dennis.sheppard
21

Wiem, że jest to dobrze po udzieleniu odpowiedzi na pytanie, ale używam innego podejścia, ponieważ używam iniekcji zależności do tworzenia instancji moich kontrolerów.

Zastąpiłem IActionInvoker (poprzez wstrzyknięcie właściwości ControllerActionInvoker kontrolera) wersją, która zastępuje metodę InvokeActionMethod.

Oznacza to brak zmian w dziedziczeniu kontrolerów i można je łatwo usunąć podczas aktualizacji do MVC4, zmieniając rejestrację kontenera DI dla WSZYSTKICH kontrolerów

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 );
            }
        }
    }
}

--- EDYCJA - Zaktualizowano, aby pokazywać rejestrację kontenera dla kontrolerów. Używam tutaj Unity.

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
źródło
Fajnie, ale jak tego używasz? Albo lepiej, jak to wstrzyknęłaś?
Adaptabi
+1 za użycie formy Stream funkcji .Serialize (). Miałem zamiar wskazać, że możesz po prostu użyć JsonConvert jak innej najlepszej odpowiedzi, ale twoje podejście stopniowo przepuszcza długie / duże obiekty - to darmowy wzrost wydajności, zwłaszcza jeśli klient podrzędny może obsługiwać częściowe odpowiedzi.
Chris Moschini,
1
fajna realizacja. To powinna być odpowiedź!
Kat Lim Ruiz
Dobra robota, to była jedyna rzecz, do której używałem podstawowego kontrolera.
Chris Diver
naprawdę fajne - jest to o wiele lepsze niż przesłanianie funkcji Json (), ponieważ w każdym miejscu, w którym zwrócisz JsonResult, zadziała i zrobi magię. Dla tych, którzy nie używają DI, po prostu dodaj chronione przesłonięcie IActionInvoker CreateActionInvoker () {return new JsonNetActionInvoker ();} do kontrolera podstawowego
Avi Pinto
13

Rozwijając odpowiedź z https://stackoverflow.com/users/183056/sami-beyoglu , jeśli ustawisz typ treści, to jQuery będzie w stanie przekonwertować zwrócone dane na obiekt dla Ciebie.

public ActionResult DoSomething()
{
    dynamic cResponse = new ExpandoObject();
    cResponse.Property1 = "value1";
    cResponse.Property2 = "value2";
    return Content(JsonConvert.SerializeObject(cResponse), "application/json");
}
StokeStoke
źródło
Dziękuję, mam mieszankę hybrydową i to jedyna rzecz, która by na mnie zadziałała.
done_merson
Użyłem tego z JSON.NET w ten sposób: JObject jo = GetJSON(); return Content(jo.ToString(), "application/json");
John Mott
6

Mój post może komuś pomóc.

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);
    }
}    
Zamglenie
źródło
Szukałem prawdziwego rozwiązania, a Ty byłaś jedyną poprawną odpowiedzią
Richard Aguirre
Dzięki. Po zaimplementowaniu własnej BaseController, była to najmniejsza zmiana wpływu - wystarczyło dodać klasę i aktualizację BaseController.
AndrewP,
4

Stworzyłem wersję, która sprawia, że ​​akcje usług internetowych są bezpieczne i proste. Używasz tego w ten sposób:

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

Klasa:

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
źródło
ale dlaczego chcesz mieć silnie typy JsonResult? : D tracisz wyniki anonimowych typów i nic nie zarabiasz po stronie klienta, ponieważ i tak nie używa klas C #?
mikus
1
@mikus Po stronie serwera jest bezpieczny dla typów: metoda musi zwracać typ MyDataContract. Wyjaśnia klientowi dokładnie, jaka struktura danych jest zwracana. Jest również zwięzły i czytelny - JsonResult <T> automatycznie konwertuje każdy typ zwracany do Json i nie musisz nic robić.
Curtis Yallop