JSON.net se serializa directamente desde oledbconnection

Actualmente tengo un controlador que toma la ruta del archivo y el nombre de la pestaña para un archivo de Excel, procesa el archivo en una tabla de datos y luego serializa la tabla en una cadena json para regresar. Esto funciona hasta que bash procesar un archivo grande, y luego recibo una excepción de falta de memoria.

Pensaba que reduciría el uso de la memoria si no cargaba todo en la tabla de datos primero y, en su lugar, cargaba directamente en la cadena json. Sin embargo, no he podido encontrar ningún ejemplo de cómo hacer esto.

¿Puedo serializar directamente desde OleDbConnection en una cadena? ¿Cómo?

public void ProcessRequest(HttpContext context) { string path = context.Request["path"]; string tableNames = context.Request["tableNames"]; string connectionString = string.Empty; if (path.EndsWith(".xls")) { connectionString = String.Format(@"Provider=Microsoft.ACE.OLEDB.12.0; Data Source={0}; Extended Properties=""Excel 8.0;HDR=YES;IMEX=1""", path); } else if (path.EndsWith(".xlsx")) { connectionString = String.Format(@"Provider=Microsoft.ACE.OLEDB.12.0; Data Source={0}; Extended Properties=""Excel 12.0 Xml;HDR=YES;IMEX=1""", path); } DbProviderFactory factory = DbProviderFactories.GetFactory("System.Data.OleDb"); DbDataAdapter adapter = factory.CreateDataAdapter(); OleDbConnection conn = new OleDbConnection(connectionString); conn.Open(); DataTable tmp = new DataTable(); DbCommand selectCommand = factory.CreateCommand(); selectCommand.CommandText = String.Format("SELECT * FROM [{0}]", tableNames); selectCommand.Connection = conn; adapter.SelectCommand = selectCommand; adapter.Fill(tmp); string tabdata = JsonConvert.SerializeObject(tmp); context.Response.Write(tabdata); } 

En primer lugar, debe dejar de serializar en una string intermedia y, en su lugar, serializar directamente en HttpResponse.OutputStream , utilizando los siguientes métodos simples:

 public static class JsonExtensions { public static void SerializeToStream(object value, System.Web.HttpResponse response, JsonSerializerSettings settings = null) { if (response == null) throw new ArgumentNullException("response"); SerializeToStream(value, response.OutputStream, settings); } public static void SerializeToStream(object value, TextWriter writer, JsonSerializerSettings settings = null) { if (writer == null) throw new ArgumentNullException("writer"); var serializer = JsonSerializer.CreateDefault(settings); serializer.Serialize(writer, value); } public static void SerializeToStream(object value, Stream stream, JsonSerializerSettings settings = null) { if (stream == null) throw new ArgumentNullException("stream"); using (var writer = new StreamWriter(stream)) { SerializeToStream(value, writer, settings); } } } 

Como una cadena grande requiere un gran bloque contiguo de memoria para la matriz de caracteres subyacente, es donde primero se va a quedar sin memoria. Ver también consejos de rendimiento de Json.NET

Para minimizar el uso de la memoria y la cantidad de objetos asignados, Json.NET admite la serialización y la deserialización directamente en una secuencia. Leer o escribir JSON de a una por vez, en lugar de tener toda la cadena JSON cargada en la memoria, es especialmente importante cuando se trabaja con documentos JSON de más de 85kb de tamaño para evitar que la cadena JSON termine en el montón de objetos grandes.

A continuación, asegúrese de envolver todos sus desechables en una statement de using , como se muestra a continuación.

Eso podría resolver su problema, pero si no lo hace, puede serializar un IDataReader a JSON utilizando el siguiente JsonConverter :

 public class DataReaderConverter : JsonConverter { public override bool CanConvert(Type objectType) { return typeof(IDataReader).IsAssignableFrom(objectType); } public override bool CanRead { get { return false; } } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var reader = (IDataReader)value; writer.WriteStartArray(); while (reader.Read()) { writer.WriteStartObject(); for (int i = 0; i < reader.FieldCount; i++) { writer.WritePropertyName(reader.GetName(i)); if (reader.IsDBNull(i)) writer.WriteNull(); else serializer.Serialize(writer, reader[i]); } writer.WriteEndObject(); } writer.WriteEndArray(); } } 

Y luego serializar para transmitir de la siguiente manera:

 public static class ExcelExtensions { private static string GetExcelConnectionString(string path) { string connectionString = string.Empty; if (path.EndsWith(".xls")) { connectionString = String.Format(@"Provider=Microsoft.ACE.OLEDB.12.0; Data Source={0}; Extended Properties=""Excel 8.0;HDR=YES;IMEX=1""", path); } else if (path.EndsWith(".xlsx")) { connectionString = String.Format(@"Provider=Microsoft.ACE.OLEDB.12.0; Data Source={0}; Extended Properties=""Excel 12.0 Xml;HDR=YES;IMEX=1""", path); } return connectionString; } public static string SerializeJsonToString(string path, string workSheetName, JsonSerializerSettings settings = null) { using (var writer = new StringWriter()) { SerializeJsonToStream(path, workSheetName, writer, settings); return writer.ToString(); } } public static void SerializeJsonToStream(string path, string workSheetName, Stream stream, JsonSerializerSettings settings = null) { using (var writer = new StreamWriter(stream)) SerializeJsonToStream(path, workSheetName, writer, settings); } public static void SerializeJsonToStream(string path, string workSheetName, TextWriter writer, JsonSerializerSettings settings = null) { settings = settings ?? new JsonSerializerSettings(); var converter = new DataReaderConverter(); settings.Converters.Add(converter); try { string connectionString = GetExcelConnectionString(path); DbProviderFactory factory = DbProviderFactories.GetFactory("System.Data.OleDb"); using (OleDbConnection conn = new OleDbConnection(connectionString)) { conn.Open(); using (DbCommand selectCommand = factory.CreateCommand()) { selectCommand.CommandText = String.Format("SELECT * FROM [{0}]", workSheetName); selectCommand.Connection = conn; using (var reader = selectCommand.ExecuteReader()) { JsonExtensions.SerializeToStream(reader, writer, settings); } } } } finally { settings.Converters.Remove(converter); } } } 

Nota: ligeramente probado. ¡Asegúrese de realizar una prueba unitaria contra su método existente antes de ponerlo en producción! Para el código del convertidor utilicé la Serialización JSON de un DataReader como inspiración.

Actualizar

Mi convertidor emite JSON en la misma estructura que el DataTableConverter de Json.NET. Por lo tanto, podrá deserializar a DataTable automáticamente utilizando Json.NET. Si prefiere un formato más compacto, puede definir uno propio, por ejemplo:

 { "columns": [ "Name 1", "Name 2" ], "rows": [ [ "value 11", "value 12" ], [ "value 21", "value 22" ] ] } 

Y ellos crean el siguiente convertidor:

 public class DataReaderArrayConverter : JsonConverter { public override bool CanConvert(Type objectType) { return typeof(IDataReader).IsAssignableFrom(objectType); } public override bool CanRead { get { return false; } } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } static string[] GetFieldNames(IDataReader reader) { var fieldNames = new string[reader.FieldCount]; for (int i = 0; i < reader.FieldCount; i++) fieldNames[i] = reader.GetName(i); return fieldNames; } static void ValidateFieldNames(IDataReader reader, string[] fieldNames) { if (reader.FieldCount != fieldNames.Length) throw new InvalidOperationException("Unequal record lengths"); for (int i = 0; i < reader.FieldCount; i++) if (fieldNames[i] != reader.GetName(i)) throw new InvalidOperationException(string.Format("Field names at index {0} differ: \"{1}\" vs \"{2}\"", i, fieldNames[i], reader.GetName(i))); } const string columnsName = "columns"; const string rowsName = "rows"; public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var reader = (IDataReader)value; writer.WriteStartObject(); string[] fieldNames = null; while (reader.Read()) { if (fieldNames == null) { writer.WritePropertyName(columnsName); fieldNames = GetFieldNames(reader); serializer.Serialize(writer, fieldNames); writer.WritePropertyName(rowsName); writer.WriteStartArray(); } else { ValidateFieldNames(reader, fieldNames); } writer.WriteStartArray(); for (int i = 0; i < reader.FieldCount; i++) { if (reader.IsDBNull(i)) writer.WriteNull(); else serializer.Serialize(writer, reader[i]); } writer.WriteEndArray(); } if (fieldNames != null) { writer.WriteEndArray(); } writer.WriteEndObject(); } } 

Por supuesto, necesitarás crear tu propio convertidor de deserialización en el lado del cliente.

Alternativamente, podrías considerar comprimir tu respuesta. Nunca lo he intentado, pero vea HttpWebRequest y GZip Http Responses y ASP.NET GZip Encoding Warning .