Publicación de datos JSON en ASP.NET MVC

Estoy tratando de obtener una lista de líneas de pedido en una página web usando JSON, que luego será manipulada y enviada al servidor mediante una solicitud ajax utilizando la misma estructura JSON que llegó (excepto que se cambiaron los valores de un campo).

Recibir datos del servidor es fácil, ¡la manipulación es aún más fácil! pero enviando esa información JSON al servidor para salvar … ¡tiempo de suicidio! POR FAVOR alguien puede ayudar!

Javascript

var lineitems; // get data from server $.ajax({ url: '/Controller/GetData/', success: function(data){ lineitems = data; } }); // post data to server $.ajax({ url: '/Controller/SaveData/', data: { incoming: lineitems } }); 

C # – Objetos

 public class LineItem{ public string reference; public int quantity; public decimal amount; } 

C # – Controlador

 public JsonResult GetData() { IEnumerable lineItems = ... ; // a whole bunch of line items return Json(lineItems); } public JsonResult SaveData(IEnumerable incoming){ foreach(LineItem item in incoming){ // save some stuff } return Json(new { success = true, message = "Some message" }); } 

Los datos llegan al servidor como datos de publicación serializados. El encuadernador de modelo automatizado intenta vincular IEnumerable incoming y sorprendentemente obtiene el resultante IEnumerable tiene el número correcto de LineItems , simplemente no los rellena con datos.

SOLUCIÓN

Usando respuestas de varias fonts, principalmente djch en otra publicación de stackoverflow y BeRecursive continuación, resolví mi problema utilizando dos métodos principales.

Lado del servidor

El deserializador a continuación requiere referencia a System.Runtime.Serialization y al using System.Runtime.Serialization.Json

  private T Deserialise(string json) { using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(json))) { var serialiser = new DataContractJsonSerializer(typeof(T)); return (T)serialiser.ReadObject(ms); } } public void Action(int id, string items){ IEnumerable lineitems = Deserialise<IEnumerable>(items); // do whatever needs to be done - create, update, delete etc. } 

Lado del cliente

Utiliza el método de stringify de json.org, disponible en esta dependencia https://github.com/douglascrockford/JSON-js/blob/master/json2.js (que es de 2.5kb cuando se lo reduce)

  $.ajax({ type: 'POST', url: '/Controller/Action', data: { 'items': JSON.stringify(lineItems), 'id': documentId } }); 

Eche un vistazo a la publicación de Phil Haack sobre el modelo de datos JSON vinculantes . El problema es que la carpeta de modelo predeterminada no serializa JSON correctamente. Necesitas algún tipo de ValueProvider O podrías escribir un encuadernador de modelo personalizado:

 using System.IO; using System.Web.Script.Serialization; public class JsonModelBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { if(!IsJSONRequest(controllerContext)) { return base.BindModel(controllerContext, bindingContext); } // Get the JSON data that's been posted var request = controllerContext.HttpContext.Request; //in some setups there is something that already reads the input stream if content type = 'application/json', so seek to the begining request.InputStream.Seek(0, SeekOrigin.Begin); var jsonStringData = new StreamReader(request.InputStream).ReadToEnd(); // Use the built-in serializer to do the work for us return new JavaScriptSerializer() .Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType); // -- REQUIRES .NET4 // If you want to use the .NET4 version of this, change the target framework and uncomment the line below // and comment out the above return statement //return new JavaScriptSerializer().Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType); } private static bool IsJSONRequest(ControllerContext controllerContext) { var contentType = controllerContext.HttpContext.Request.ContentType; return contentType.Contains("application/json"); } } public static class JavaScriptSerializerExt { public static object Deserialize(this JavaScriptSerializer serializer, string input, Type objType) { var deserializerMethod = serializer.GetType().GetMethod("Deserialize", BindingFlags.NonPublic | BindingFlags.Static); // internal static method to do the work for us //Deserialize(this, input, null, this.RecursionLimit); return deserializerMethod.Invoke(serializer, new object[] { serializer, input, objType, serializer.RecursionLimit }); } } 

Y dígale a MVC que lo use en su archivo Global.asax:

 ModelBinders.Binders.DefaultBinder = new JsonModelBinder(); 

Además, este código utiliza el tipo de contenido = ‘application / json’, así que asegúrate de configurarlo en jquery de la siguiente manera:

 $.ajax({ dataType: "json", contentType: "application/json", type: 'POST', url: '/Controller/Action', data: { 'items': JSON.stringify(lineItems), 'id': documentId } }); 

La forma más simple de hacer esto

Le pido que lea esta publicación de blog que aborda directamente su problema.

El uso de carpetas de modelo personalizado no es realmente inteligente como señaló Phil Haack (su publicación de blog también está vinculada en la publicación superior del blog).

Básicamente tienes tres opciones:

  1. Escriba una JsonValueProviderFactory y use una biblioteca del lado del cliente como json2.js para comunicarse directamente con JSON.

  2. Escriba una JQueryValueProviderFactory que comprenda la transformación de objetos JQueryValueProviderFactory JSON que ocurre en $.ajax o

  3. Utilice el complemento jQuery muy simple y rápido descrito en la publicación del blog, que prepara cualquier objeto JSON (incluso las matrices que se vincularán a IList y las fechas que analizarán correctamente en el servidor como instancias DateTime ) que serán comprendidas por Asp.net MVC carpeta de modelo predeterminada.

De los tres, el último es el más simple y no interfiere con el funcionamiento interno de Asp.net MVC, lo que reduce la posible superficie de error. El uso de esta técnica descrita en la publicación del blog correlacionará correctamente los parámetros de acción de tipo fuerte y los validará también. Entonces, básicamente es una situación en la que todos ganan.

En MVC3 agregaron esto.

Pero lo que es aún más agradable es que, dado que el código fuente de MVC está abierto, puede tomar el ValueProvider y usarlo usted mismo en su propio código (si todavía no está en MVC3).

Terminarás con algo como esto

 ValueProviderFactories.Factories.Add(new JsonValueProviderFactory()) 

Resolví este problema siguiendo los consejos de vestigal aquí:

¿Puedo establecer una longitud ilimitada para maxJsonLength en web.config?

Cuando necesitaba publicar un JSON grande en una acción en un controlador, obtenía el famoso “Error durante la deserialización usando JSON JavaScriptSerializer. La longitud de la cadena excede el valor establecido en la propiedad maxJsonLength. \ R \ nNombre del parámetro: entrada proveedor de valor “.

Lo que hice fue crear una nueva ValueProviderFactory, LargeJsonValueProviderFactory, y establecer MaxJsonLength = Int32.MaxValue en el método GetDeserializedObject

 public sealed class LargeJsonValueProviderFactory : ValueProviderFactory { private static void AddToBackingStore(LargeJsonValueProviderFactory.EntryLimitedDictionary backingStore, string prefix, object value) { IDictionary dictionary = value as IDictionary; if (dictionary != null) { foreach (KeyValuePair keyValuePair in (IEnumerable>) dictionary) LargeJsonValueProviderFactory.AddToBackingStore(backingStore, LargeJsonValueProviderFactory.MakePropertyKey(prefix, keyValuePair.Key), keyValuePair.Value); } else { IList list = value as IList; if (list != null) { for (int index = 0; index < list.Count; ++index) LargeJsonValueProviderFactory.AddToBackingStore(backingStore, LargeJsonValueProviderFactory.MakeArrayKey(prefix, index), list[index]); } else backingStore.Add(prefix, value); } } private static object GetDeserializedObject(ControllerContext controllerContext) { if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase)) return (object) null; string end = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd(); if (string.IsNullOrEmpty(end)) return (object) null; var serializer = new JavaScriptSerializer {MaxJsonLength = Int32.MaxValue}; return serializer.DeserializeObject(end); } /// Returns a JSON value-provider object for the specified controller context. /// A JSON value-provider object for the specified controller context. /// The controller context. public override IValueProvider GetValueProvider(ControllerContext controllerContext) { if (controllerContext == null) throw new ArgumentNullException("controllerContext"); object deserializedObject = LargeJsonValueProviderFactory.GetDeserializedObject(controllerContext); if (deserializedObject == null) return (IValueProvider) null; Dictionary dictionary = new Dictionary((IEqualityComparer) StringComparer.OrdinalIgnoreCase); LargeJsonValueProviderFactory.AddToBackingStore(new LargeJsonValueProviderFactory.EntryLimitedDictionary((IDictionary) dictionary), string.Empty, deserializedObject); return (IValueProvider) new DictionaryValueProvider((IDictionary) dictionary, CultureInfo.CurrentCulture); } private static string MakeArrayKey(string prefix, int index) { return prefix + "[" + index.ToString((IFormatProvider) CultureInfo.InvariantCulture) + "]"; } private static string MakePropertyKey(string prefix, string propertyName) { if (!string.IsNullOrEmpty(prefix)) return prefix + "." + propertyName; return propertyName; } private class EntryLimitedDictionary { private static int _maximumDepth = LargeJsonValueProviderFactory.EntryLimitedDictionary.GetMaximumDepth(); private readonly IDictionary _innerDictionary; private int _itemCount; public EntryLimitedDictionary(IDictionary innerDictionary) { this._innerDictionary = innerDictionary; } public void Add(string key, object value) { if (++this._itemCount > LargeJsonValueProviderFactory.EntryLimitedDictionary._maximumDepth) throw new InvalidOperationException("JsonValueProviderFactory_RequestTooLarge"); this._innerDictionary.Add(key, value); } private static int GetMaximumDepth() { NameValueCollection appSettings = ConfigurationManager.AppSettings; if (appSettings != null) { string[] values = appSettings.GetValues("aspnet:MaxJsonDeserializerMembers"); int result; if (values != null && values.Length > 0 && int.TryParse(values[0], out result)) return result; } return 1000; } } } 

Luego, en el método Application_Start de Global.asax.cs, reemplace ValueProviderFactory con el nuevo:

 protected void Application_Start() { ... //Add LargeJsonValueProviderFactory ValueProviderFactory jsonFactory = null; foreach (var factory in ValueProviderFactories.Factories) { if (factory.GetType().FullName == "System.Web.Mvc.JsonValueProviderFactory") { jsonFactory = factory; break; } } if (jsonFactory != null) { ValueProviderFactories.Factories.Remove(jsonFactory); } var largeJsonValueProviderFactory = new LargeJsonValueProviderFactory(); ValueProviderFactories.Factories.Add(largeJsonValueProviderFactory); } 

Puedes probar estos. 1. stringify su objeto JSON antes de llamar a la acción del servidor a través de ajax 2. deserializar la cadena en la acción y luego utilizar los datos como un diccionario.

Muestra de JavaScript a continuación (envío del objeto JSON

 $.ajax( { type: 'POST', url: 'TheAction', data: { 'data': JSON.stringify(theJSONObject) } }) 

Muestra de acción (C #) a continuación

 [HttpPost] public JsonResult TheAction(string data) { string _jsonObject = data.Replace(@"\", string.Empty); var serializer = new System.Web.Script.Serialization.JavaScriptSerializer(); Dictionary jsonObject = serializer.Deserialize>(_jsonObject); return Json(new object{status = true}); } 

Si tienes los datos JSON entrando como una cadena (por ejemplo, ‘[{“id”: 1, “nombre”: “Charles”}, {“id”: 8, “nombre”: “John”}, { “id”: 13, “nombre”: “Sally”}] ‘)

Entonces usaría JSON.net y usaría Linq para JSON para obtener los valores …

 using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using Newtonsoft.Json; using Newtonsoft.Json.Linq; public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (Request["items"] != null) { var items = Request["items"].ToString(); // Get the JSON string JArray o = JArray.Parse(items); // It is an array so parse into a JArray var a = o.SelectToken("[0].name").ToString(); // Get the name value of the 1st object in the array // a == "Charles" } } } 

La respuesta de BeRecursive es la que yo utilicé, para poder estandarizar en Json.Net (tenemos MVC5 y WebApi 5 – WebApi 5 ya usa Json.Net), pero encontré un problema. Cuando tiene parámetros en la ruta hacia la cual está realizando la POST, MVC intenta llamar a la carpeta de modelos para los valores de URI, y este código intentará vincular la JSON publicada a esos valores.

Ejemplo:

 [HttpPost] [Route("Customer/{customerId:int}/Vehicle/{vehicleId:int}/Policy/Create"] public async Task Create(int customerId, int vehicleId, PolicyRequest policyRequest) 

La función BindModel se llama tres veces, bombardeando la primera, ya que intenta vincular el JSON al ID de customerId con el error: Error reading integer. Unexpected token: StartObject. Path '', line 1, position 1. Error reading integer. Unexpected token: StartObject. Path '', line 1, position 1.

BindModel este bloque de código a la parte superior de BindModel :

 if (bindingContext.ValueProvider.GetValue(bindingContext.ModelName) != null) { return base.BindModel(controllerContext, bindingContext); } 

El ValueProvider, afortunadamente, tiene valores de ruta calculados para cuando llega a este método.

Lo resolví usando una deserialización “manual”. Voy a explicar en código

 public ActionResult MyMethod([System.Web.Http.FromBody] MyModel model) { if (module.Fields == null && !string.IsNullOrEmpty(Request.Form["fields"])) { model.Fields = JsonConvert.DeserializeObject(Request.Form["fields"]); } //... more code }