Test de Carga: Web Performance Test con Visual Studio 2010

por Diego Rosso  22. diciembre 2011

 

Pequeña introducción

Continuando con el post anterior vamos a mostrar cómo podemos realizar pruebas de carga sobre nuestras aplicaciones web utilizando Visual Studio 2010 en forma más detallada. La idea principal es ver cómo responde nuestra web ante varias peticiones concurrentes y obtener información sobre los Performance Counters de nuestros servidores que nos servirán de base para detectar problemas en la aplicación o en los servidores donde está alojada.

Que son los Performance Counters?

Los contadores de rendimiento son objetos del sistema operativo que nos muestran información sobre el rendimiento del hardware y software, como por ejemplo la memoria, el disco y los procesos.

Un contador es una medida de un parámetro determinado. Estos se organizan en grupos como por ejemplo los que vienen con el sistema operativo que están comprendidos dentro del objeto System y otros que se instalan como los de SQL Server.

Windows trae una herramienta para acceder a los objetos y sus contadores, la misma se llama Performance Monitor, la cual podemos accederla yendo a Incio --> Ejecutar --> y escribir Perform.

 

 

LoadTest1

 

WebTests

Para realizar nuestras pruebas de stress lo primero que debemos hacer es crear nuestros Webtest, en el cual se definen cuáles son las urls sobre las que van a realizarse las pruebas. Algo muy importante es que deben tener instalada la versión Ultimate del Visual Studio 2010 para poder contar con estos tipos de test (la versión 2008 del Visual Studio ya contaba con esta funcionalidad).

Pasos para crearlo.

1. En el visual Studio vamos a crear un nuevo proyecto del tipo Test, para eso ir a New –> Proyect  --> Test --> Test project y generamos el proyecto base de test.

 

LoadTest2

 

2. Eliminamos la clase que nos agrega por default.

 

LoadTest3

 

3. Agregamos un nuevo test del tipo Web Performance Test, haciendo botón derecho sobre el proyecto –> Add --> Web Performance Test

 

LoadTest4

Al hacer esto se nos abrirá el Web Test Recorder, donde debemos ingresar la dirección web que deseamos testear, en nuestro ejemplo ingresaremos www.google.com.ar y vamos a buscar VEMN y veremos como el Recorder va guardando las urls junto con sus parámetros gets, los cuales están dentro de una carpeta llamada QueryStrings Parameters.

 

LoadTest5

Luego cerraremos esta ventana y vamos a quedarnos a nivel de ejemplo con solo la primer URL y sus parámetros.

 

LoadTest6

 

 

 

Ahora debemos agregar a nuestro proyecto un Load test (test de carga) que será el encargado de simular múltiples peticiones sobre la URL que cargamos en el Webtest. Para esto hacemos botón derecho sobre el proyecto y seleccionamos add-->Load Test y se nos abrirá un wizard con su clásica ventana de bienvenida le damos next para para a configurar el test de carga.

 

LoadTest7

 

Lo primero que nos pide es un nombre para el escenario de test que vamos a crear y nos pide seleccionar que tipo de pausas queremos entre la ejecución de las distintas iteraciones, en el ejemplo lo dejaremos como viene por default y le pasaremos al próximo paso.

 

LoadTest8

En este caso nos pide que seleccionemos el patrón de carga el cual puede ser constante donde le definimos la cantidad de usuarios que debe simular de forma constante durante el transcurso de toda la prueba, o incremental que es el que vamos a elegir nosotros para nuestro ejemplo, los parámetros que nos pide son los siguientes

  • · Start User Count: Cantidad de usuarios que se generaran cuando comience la prueba, en nuestro caso pondremos 20.
  • · Step Duration:  Donde definimos la cantidad de segundos que van a pasar hasta que se agreguen más peticiones sobre la URL que estamos testeando, en nuestro caso 15 segundos.
  • · Step User Count:  Cantidad de usuarios que se agregaran una vez transcurrido el tiempo configurado en Step Duration.
  • · Maximun User Count: Donde definimos la cantidad máxima de usuarios a simular.

 

LoadTest9

 

Luego nos pide el modelo a utilizar por el test de carga, en nuestro caso elegiremos el primer modelo basado a partir del número total de pruebas. Podemos ver aquí un detalle de cada uno de estos modelos.

 

 

LoadTest10

 

En el próximo paso del asistente debemos elegir los webtest que deseamos testear en el presente escenario, en nuestro caso vamos a elegir el único que tenemos pero podríamos seleccionar varios y asignar la distribución para cada uno de estos.

 

LoadTest11

Ahora ingresamos las distintas formas de conexión que deseamos testear y la distribución que queremos darle a cada uno.

 

LoadTest12

 

En el próximo paso seleccionaremos los distintos browsers que queremos que el test de carga simule y la forma en que se distribuirá la carga.

 

LoadTest13

 

Ahora debemos ingresar el nombre del equipo que deseamos supervisar que debería ser donde se encuentra alojado el sitio web que estamos probando, este debe ser un servidor el cual se encuentre en nuestra red o nuestra propia máquina. Si nuestro sitio tiene distintos servidores como por ejemplo uno para la BD y otro para donde se encuentra alojado el sitio, podemos agregar ambos haciendo click en add computer.

Debemos seleccionar los performance counter que deseamos monitorear en cada uno de los equipos.

 

 

 

LoadTest14

En el último paso del Wizard, nos pide la duración del test que en nuestro caso lo vamos a dejar en 10 minutos y también podemos configurar si queremos que nos genere un log cada vez que un test falle, entre otras cosas.

Presionamos sobre Finish y podemos ver como quedo configurado nuestro test de carga.

 

LoadTest15

 

Listo! Estamos en condiciones de poner a correr nuestro test y ver el comportamiento de los performance counters de los diferentes equipos supervisados.

 

LoadTest16

 

En nuestro próximo post vamos a ver cómo generar el código de nuestros Webtest para hacerlos más dinámicos y como pasar parámetros a la URL a través de distintas fuentes como pueden ser base de datos, archivos csv o xmls.

Tags:

.NET | Desarrollo Web | Testing | Visual Studio

Cuando se acaba el soporte de Microsoft a un producto?

por Daniel Laco  15. abril 2011

 

A menudo nos encontramos en las empresas con aplicaciones, software y tecnologías que les queda poco tiempo de soporte de parte de Microsoft.

En otros cosas el tema es mas complicado porque ya no tienen directamente soporte de ningún tipo.

Hablando con un Arquitecto el otro día me comentaba que tenían varias aplicaciones desarrolladas en .NET 1.1, y que no conseguía que la gerencia encarara un proyecto de migración a versiones mas nuevas de .NET

En el caso de la versión 1.1 ya no tiene mas soporte de MS en cuanto a liberacion de Services Pack por temas de errores del código, y solamente le queda 1 año y medio aproximadamente de soporte extendido (solo sacan hotfixes por temas de seguridad).

Creo que esto es argumento mas que suficiente para para encarar un proyecto de migración serio.

Ni hablar de compañias donde hay muchas aplicaciones corriendo en VB6, Visual Fox, etc.

Aquí les dejo el ink http://support.microsoft.com/lifecycle/search/Default.aspx donde pueden consultar el estado de los productos dentro del ciclo de vida de MS.

 

 

Aquí también dejo un artículo con mas explicación sobre el tema y que fué el que me motivó a escribir esta nota. (http://www.ewaldhofman.nl/post/2011/04/14/When-runs-a-product-out-of-support.aspx)

Tags: ,

.NET

Visual Studio 2010 and .NET Framework 4 Training Kit

por Victor Passador  26. marzo 2011

Ya van quedando cada vez menos excusas para no estar entrenado con lo último. Acaba de salir publicado un Training Kit gratuito para VS 2010 y .NET 4.0.

Es un download de un poco más de 400MB que en disco se transforman en casi 2GB de presentaciones, papers, hand-on-labs, código de ejemplo, etc.

Pueden descargarlo desde aquí.

Enjoy !

Tags: ,

.NET | Visual Studio

Como obtener el Connection String clásico con Entity Framework

por Andres Stang  28. enero 2011

Como sabrán Entity Framework utiliza un conection string para saber con qué base de datos trabajar, el tema es que además de contener la cadena de conexión que conocemos de toda la vida, tiene un poco más de información que utiliza para indicarle otras cosas que no vienen al caso.

Si quisiéramos recuperar la clásica cadena de conexión para, por ejemplo, no utilizar Entity Framework y acceder directamente a la base de datos con ADO.NET uno pensaria en hacer:

 

   1:  MiModeloEntities miEntities = new MiModeloEntities();
   2:  string connStringEF = miEntities.Connection.ConnectionString;

 

Pero no nos serviría! Ya que tenemos la cadena de conexión de Entity Framework que incluye metadata que antes mencionamos. Ya sé, seguramente ya estás pensando en usar expresiones regulares… Momento! La solución? Simplemente:

 

   1:  MiModeloEntities miEntities = new MiModeloEntities();
   2:  string connString = ((System.Data.EntityClient.EntityConnection)miEntities.Connection).StoreConnection.ConnectionString;

 

Y de esta manera nos evitamos tener duplicada la información en nuestra configuración y evitamos parsear el string innecesariamente.

Tags: ,

.NET | Entity Framework | ORM | SQL Server

Seguridad y Autorizacion centralizada en base de datos con Asp.Net MVC

por Andres Stang  22. diciembre 2010

La palabra Seguridad es siempre una consulta que recibo cuando hablo de cómo programar en Asp.Net, y siempre me gusta dar el mismo chiste como respuesta: “Vamos por partes decía Jack el Destripador”. Es importante dividir un problema grande en problemas más pequeños y por ende más manejables, y hablando de seguridad la fórmula es sencilla.

 

SEGURIDAD = AUTENTICACIÓN + AUTORIZACIÓN


Es decir, tenes que resolver por un lado como identificar al usuario, para saber que es quien dice ser que es, y por el otro lado una vez que sepamos su identidad, tenemos que decidir si lo dejamos acceder o no.

Cuando creamos un proyecto nuevo de MVC, el template nos da una GRAN ayuda y propone resolver (mejor dicho, ya lo implementa y lo resuelve por defecto) cada tema con un componente específico. Para la autenticación utiliza Asp.Net Membership y para la autorización el decorador [Authorize].

Ambos componentes funcionan maravillosamente pero hay uno que me gusta y otro que no. Concretamente Asp.Net Membership me parece perfecto para resolver una gran cantidad de escenarios y contextos, y generalmente lo utilizo en mis proyectos. Pero el decorador de autorización a mi criterio tiene dos grandes falencias:

  • Falta de flexibilidad: el componente de autorización puede recibir como parámetro el usuario y el rol, pero esto queda explícito en el código, es decir, si me surge la necesidad de agregar un nuevo rol al sistema deberé recorrer el código, cambiarlo, recompilar y realizar un deploy del sitio.
  • Verborragico: el componente requiere gran escritura y definición por parte del programador. Esto, mas allá de la poca elegancia del código, define que sea el programador quien decide quien esté autorizado a entrar a cada acción. Y ante un error, descuido o falta de memoria del programador podemos comprometer a nuestra aplicación dejando expuesta funcionalidad sensible a usuarios indeseados.

 

¿Cómo resolvemos esto?


Allá por el año 2007 Daniel Laco escribió uno de los post más consultados de este sitio:


Autenticación de usuarios basada en Roles utilizando HTTPModules en ASP.NET


De forma muy sencilla y elegante resuelve las dos falencias presentadas. Y si bien el código no es compatible con todas las versiones del .net framework, o con el paradigma MVC, el concepto si aplica en el universo Asp.net.

Lo que haremos el día de hoy, es actualizar un poco el código para que puede ser utilizado en nuestros proyectos de MVC ;)

Lo primero que tenemos que hacer es programar el método de nuestro modulo, quedaría algo así:

 

 public void Init(HttpApplication context)
 {
     context.AuthorizeRequest += new EventHandler(OnAuthorizeRequest);
 }
 
 void OnAuthorizeRequest(object sender, EventArgs e)
 {
     HttpContext context = ((HttpApplication)sender).Context;
 
     RouteData routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(context));
 
     if (routeData != null)
     {
         string controller = routeData.GetRequiredString("controller");
         string action = routeData.GetRequiredString("action");
 
         if (!EstaAutorizado(controller, action, context.User))
         {
             /// 401 es el codigo de resultado HTTP para acceso no autorizado
             context.Response.StatusCode = 401; 
             context.Response.End();
         }
     }
 }

 

En el código vemos cómo recuperar la información de la ruta para saber que método estamos ejecutando, y luego definir que si no se encuentra autorizado el usuario, le responderemos con un código HTTP estándar: 401 (lista de códigos)

De esta forma dejamos actuar al framework de asp.net que ya tiene resuelto el enganche y resolución del caso. Por ejemplo, MVC viene configurado por defecto que ante un error de autenticación redirija al usuario automáticamente a la página de login:

 

<authentication mode="Forms">
   <forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>

 

Lo único que nos falta, es recordar agregar el modulo a nuestro web.config para que se ejecute:

<httpModules>
  <add name="MiModulo" type="AutorizacionMVCHttpModule.Models.MiModulo"/>
</httpModules>

 

Listo! Con muy pocas líneas hemos resuelto de manera elegante y limpia un requerimiento esencial y controversial en la mayoría de nuestros proyectos.

Les dejo el código fuente de ejemplo para que lo puedan descargar y ver implementado
Download AutorizacionMVCHttpModule.zip (262,77 kb)

Happy coding!

Tags: , , , , ,

.NET | ASP.NET | Desarrollo Web

10 años como Microsoft MVP (Most Valuable Professional)!

por Daniel Laco  1. octubre 2010

Nuevamente he sido reconocido como MVP (Most Valuable Professional) por Microsoft Corp.

Es un honor contar con tal distinción por 10 años desde el 2001.

Pueden encontrar aquí un resumen de todo lo hecho en este tiempo.

Me toca agradecer al equipo de DPE de Microsoft Argentina (Miguel Saenz y Eduardo Mangarelli) por tan distinguido reconocimiento y a mi MVP Lead Fernando Garcia Loera

Gracias nuevamente, por mi parte seguiré esforzándome y compartiendo el conocimiento técnico con las comunidades y demás interesados en la tecnología Microsoft.

 

MVP_Horizontal_FullColor

Tags:

.NET | General

A pensar en paralelización: Parallel Extensions

por Gustavo Lombardo  29. julio 2010

Si deseamos que nuestro software siga beneficiándose de las capacidades futuras de hoy y mañana de los procesadores debemos empezar a cambiar nuestra manera de desarrollar. La razón de esto es que las aplicaciones de negocio van tener que ejecutarse en máquinas multi-core o con varios procesadores. Hasta hace pocos años una máquina con varios procesadores estaba prácticamente reservada a la supercomputación, pero hoy en día ya son comunes en cualquier PC de escritorio.

La programación concurrente y el paralelismo no son una materia trivial, la sincronización entre procesos no lo es y dividir el trabajo, asignarlo a varios procesadores, recoger los resultados y mezclarlos para reconstruir la solución final tampoco.

En el pasado, la paralelización requería manipulación de bajo nivel de los subprocesos y bloqueos. Hoy en día, los desarrolladores no tendrían que luchar contra esta complejidad, porque sería dar un paso atrás en productividad. Su tarea es seguir dedicando sus esfuerzos a lo que mejor saben hacer que es desarrollar aplicaciones de alto nivel que resuelven problemas de negocio sin controlar la ejecución concurrente de sus procesos.

Para ello, debemos acostumbrarnos a usar el término task en lugar del thread como seguramente la mayoría de nosotros veníamos haciendo y aprovechar las nuevas capacidades que ofrece las APIs de .NET Framework 4, así como también las del Visual Studio 2010.

.NET Framework 4, proporciona un nuevo runtime, nuevos tipos de biblioteca de clases y nuevas herramientas de diagnóstico para la programación paralela y concurrente. Es aquí donde hace su aparición Parallel Extensions, simplificando el desarrollo en paralelo, de modo de poder escribir código paralelo eficaz, específico y escalable de forma natural sin tener que trabajar directamente con subprocesos. Sin embargo, no todo código se presta para la paralelización por ejemplo, si un bucle realiza solo una cantidad reducida de trabajo en cada iteración o no se ejecuta para un gran número de iteraciones, la sobrecarga de la paralelización puede dar lugar a una ejecución más lenta del código. Además, al igual que cualquier código multiproceso, la paralelización hace que la ejecución del programa sea más compleja.

Parallel Extensions se compone de dos partes: Parallel Linq (PLINQ) y Task Parallel Library (TPL). También consta de un conjunto de estructuras de datos de coordinación (CDS) utilizada para sincronizar y coordinar la ejecución de tareas concurrentes. La siguiente ilustración proporciona información general de alto nivel de la arquitectura de programación paralela en .NET Framework 4.

IC389193

A continuación explicaré cada uno de sus componentes. PLINQ implementa el conjunto completo de operadores de consulta estándar de LINQ  e incluye otros adicionales para las operaciones paralelas. Combina la simplicidad y legibilidad de la sintaxis de LINQ con la eficacia de la programación paralela. En muchos escenarios, PLINQ puede aumentar significativamente la velocidad de las consultas LINQ  to Objects utilizando todos los núcleos disponibles en el equipo host de una forma más eficaz. Para hacer esto divide el origen de datos en segmentos y, a continuación, ejecuta la consulta en cada segmento en subprocesos de trabajo independientes en paralelo en varios procesadores. La clase System.Linq.ParallelEnumerable expone casi toda la funcionalidad de PLINQ.  Les mostraré un ejemplo en el que invoco el método de extensión ParallelEnumerableAsParallel() para indicarle que la query debe ejecutarse en forma paralela.

for (var i = 0; i < data.Length; i++)
{
    data[i] = i;
}
 
data[1000] = -1;
data[14000] = -2;
data[15000] = -3;
data[676000] = -4;
data[8024540] = -5;
data[9908000] = -6;
 
var negativos = from valor in data.AsParallel()
                where valor < 0
                select valor;

 

Al ejecutarse la query en paralelo los resultados de cada subproceso de trabajo deben volver a combinarse en el subproceso principal por ejemplo para la inserción en una lista. El tipo de combinación que  PLINQ realiza depende de los operadores que se encuentran en la consulta. Con el método WithParallelMergeOptions(), puede indicarse a PLINQ una sugerencia de que tipo de combinación se va a realizar.

La biblioteca TPL es un conjunto de API que tiene como propósito lograr que los desarrolladores aumenten la productividad simplificando el proceso de agregar paralelismo y simultaneidad a las aplicaciones, utilizando eficazmente todos los procesadores que están disponibles. Además, se encarga de la división del trabajo, la programación de los subprocesos y otros detalles de bajo nivel.

El concepto principal de Parallel Extensions es una tarea, que es una pequeña unidad de código, de forma independiente. Se encarga de dividir el trabajo y lanzar un número óptimo de threads basándose en el número de CPUs o cores que tenemos.

Yendo aun mas allá, sin llegar al nivel de granularidad de una task, podemos trabajar a más alto nivel aún con la clase estática Parallel y escribir código como el siguiente:

static void Main()
{
    Parallel.For(from, to, i=> )
    {
    });
}
 

Mediante un ciclo como el anterior, dejamos que el runtime de Parallel Extensions se encargue de paralelizar el trabajo creando las tasks y encargándose de  todo el proceso. A continuación les mostraré las mejoras de rendimiento al paralelizar un programa muy sencillo. El método SumaRaicesNesimaDeX devuelve la suma de las raíces n-énesima de todos los enteros entre 1 y un 10 millones donde n es una variable recibida como parámetro. La forma de resolución de la raíz contiene pasos de procesamiento adicionales que aumentan el tiempo de ejecución total de la consulta. Además, en el main utilizo la clase Stopwatch para determinar cuantos milisegundos tarda el programa en ejecutarse.

static void Main()
{
    var tiempo = Stopwatch.StartNew();
    
    for (var i = 2; i < 20; i++)
    {
        var resultado = SumaRaicesNesimaDeX(i);
        Console.WriteLine("Raiz {0} = {1} ", i, resultado);
    }
 
    Console.WriteLine(tiempo.ElapsedMilliseconds);
    Console.ReadLine();
}
 
public static double SumaRaicesNesimaDeX(int n)
{
    double resultado = 0;
 
    for (var x = 1; x < 10000000; x++)
    {
        //Raíz n-ésima de x
        resultado += Math.Exp(Math.Log(x) / n);
    }
 
    return resultado;
}
 

El programa lo ejecute en AMD Phenom II X4 810 de 64 bits con 4 GB de memoria RAM y tardó aproximadamente 16 segundos.

Ahora si remplazo el ciclo:

for (var i = 2; i < 20; i++)
{
    var resultado = SumaRaicesNesimaDeX(i);
    Console.WriteLine("Raiz {0} = {1} ", i, resultado);
}

 

por el siguiente código añadiéndole paralelismo:

Parallel.For(2, 20, (i) =>
{
    var resultado = SumaRaicesNesimaDeX(i);
    Console.WriteLine("Raiz {0} = {1} ", i, resultado);
});

Mediante pequeñas modificaciones del código como son un índice de inicio y de fin y la llamada a través de un delegate, la ejecución del programa solo tarda aproximadamente 4 segundos y medio.

Visual Studio 2010 incluye nuevas herramientas de profiling y debugging para la ejecución concurrente. Un ejemplo de esto es el visualizador de concurrencia. Esta herramienta ayuda  a analizar nuestras aplicaciones secuenciales para descubrir las oportunidades de paralelismo. El visualizador de concurrencia incluye visualización y herramientas de reporte. Hay tres puntos de vista principales: utilización de CPU, subprocesos y núcleos.

imageEl eje X muestra el tiempo transcurrido desde el inicio de la traza hasta el final de la actividad de la aplicación. El eje Y muestra el número de núcleos de procesadores lógicos en el sistema. La zona verde representa el número medio de cores lógicos que se analiza en la aplicación en un momento dado en la ejecución del profiling. El resto de los núcleos por otros procesos que se ejecutan en el sistema (que se muestra en amarillo).

Si queremos paralelizar nuestra aplicación, debemos buscar áreas de ejecución que presentan largas regiones verdes a nivel de un solo núcleo en el eje Y o regiones donde no hay mucha utilización de la CPU, donde el verde no demuestra ni es considerablemente menos de 1 en promedio. Ambas circunstancias podrían indicar una oportunidad para la paralelización. En segundo lugar, si estamos tratando de afinar la aplicación paralela, esta vista le permite confirmar el grado de paralelismo que existe cuando la aplicación se ejecuta.

Este cambio de paradigma nos exige una nueva manera de pensar. Los threads son una abstracción demasiado débil. Debemos empezar a pensar en tareas no en threads y en relaciones entre esas tareas y su concurrencia en lugar de sincronización de threads. Seremos los desarrolladores quienes de un modo u otro tendremos que empezar a pensar en paralelización.

Tags:

.NET | Entity Framework | Visual Studio

Enviando JSON a una acción en ASP.NET MVC

por fernando  22. abril 2010

 

Descargar Código Fuente

El otro día estaba intentando enviar datos a una acción utilizando el AJAX de jQuery. En la mayoría de los casos, podrán utilizar este método y enviar datos a una acción sin problemas, ya que el default binder de MVC se encarga de interpretar los datos.

Mi problema surgió cuando intenté enviar datos más complejos. En el controlador tenía el siguiente método:

   1:          [AcceptVerbs(HttpVerbs.Post)]
   2:          public virtual JsonResult Actualizar(Producto producto)
   3:          {
   4:              //HACER ALGO 
   5:          }

Este método sólo acepta un POST, y tiene un argumento de tipo Producto. Veamos cómo está compuesta esta clase:

   1:      public class Producto
   2:      {
   3:          public int Id { get; set; }
   4:          public string Nombre { get; set; }
   5:          public IEnumerable<Promocion> Promociones { get; set; }
   6:   
   7:          public class Promocion
   8:          {
   9:              public string Id { get; set; }
  10:              public double Descuento { get; set; }
  11:              public DateTime FechaDesde { get; set; }
  12:              public DateTime FechaHasta { get; set; }
  13:          }
  14:      }

 

Para poder enviar esto utilizando jQuery y AJAX, lo más sencillo sería hacer:

   1:      var data = {
   2:          "Id": 1,
   3:          "Nombre": "Cualquier Nombre",
   4:          "Promociones": [
   5:         {
   6:             "Id": 1,
   7:             "Descuento": 0.15,
   8:             "FechaDesde": new Date(año, mes, día),
   9:             "FechaHasta": new Date(año, mes, día)
  10:         },
  11:         {
  12:             "Id": 2,
  13:             "Descuento": 0.05,
  14:             "FechaDesde": new Date(año, mes, día),
  15:             "FechaHasta": new Date(año, mes, día)
  16:         }
  17:       ]
  18:      };
  19:   
  20:      $.ajax({
  21:          type: "POST",
  22:          dataType: "json", // Tipo de dato que devuelve el server
  23:          url: 'Controller/Action',
  24:          data: data, // Datos a enviar
  25:          success: function(serverResponse) {
  26:              // Realizar algo cuando la llamada es exitosa
  27:          },
  28:          error: function() {
  29:              // Realizar algo cuando la llamada falla
  30:          }
  31:      });
 

Lo que estoy haciendo es crear un objeto con los miembro Id, Nombre y Promociones y asignándolo a la variable data.

Sin embargo, el miembro Promociones es un objeto de tipo Array. Por default, la función $.ajax() intentará convertir cualquier dato a un string. Como Promociones es un array, al intentar convertirlo a un string, terminará enviando al servidor lo siguiente:

IDDivision=6&IDBonificacion=LB6&Tipo=A&Vigencias=[object+Object]&Vigencias=[object+Object]

Es decir, jQuery serializa los valores de un mismo array con el mismo Key. Por ejemplo, {foo:["bar1", "bar2"]} se convierte en '&foo=bar1&foo=bar2'.

Sin embargo, como el valor de cada elemento de Promociones es otro objeto, lo que termina enviando es el .toString() del objeto, el cual es "[object Object]" justamente. El Default Model Binder de ASP.NET MVC, intentará hacer el bind al objeto Producto, pero debido a que jQuery serializo incorrectamente el objeto, el miembro Promociones terminará siendo null, en lugar de los datos que estábamos intentando enviar realmente.

Para resolver este problema, podemos convertir los objetos javascript a JSON. Para hacer esto, utilicé JSON for jQuery (link), pero se puede utilizar cualquier cosa que tome un objeto javascript y lo transforme a JSON. Al enviar los datos al server, debemos asegurarnos que el content type sea del tipo application/json, que es lo que estoy haciendo en la línea 5 del siguiente código:

 

   1:      var jsonString = $.toJSON(data);
   2:      $.ajax({
   3:          type: "POST",
   4:          dataType: "json", // Tipo de dato que devuelve el server
   5:          contentType: 'application/json', // Tipo de datos que envío
   6:          url: 'Controller/Action',
   7:          data: jsonString, // Datos a enviar
   8:          success: function(serverResponse) {
   9:              // Realizar algo cuando la llamada es exitosa
  10:          },
  11:          error: function() {
  12:              // Realizar algo cuando la llamada falla
  13:          }
  14:      });

Fíjense que antes de hacer la llamada a ajax(), estoy transformando la variable data a JSON, y pasando su resultado a la función $.ajax(), en lugar del objeto. La variable jsonString entonces quedará en:

'{"Id": 1, "Nombre": "Cualquier Nombre", "Promociones": [{ "Id": 1, "Descuento": 0.15, "FechaDesde": "2010-01-01T03:00:00.000Z", "FechaHasta": "2010-05-31T03:00:00.000Z" }, { "Id": 2, "Descuento": 0.05, "FechaDesde": "2010-06-02T03:00:00.000Z", "FechaHasta": "2010-08-01T03:00:00.000Z" } ] }'

Si prestan atención, podrán darse cuenta que el JSON string es exactamente igual que la declaración de la variable data, definida más arriba. La única diferencia es que jsonString es literalmente un string, y data es un objeto javascript.

Ahora, lo único que queda resolver es el Model Binding del lado del servidor. Lamentablemente, ASP.NET MVC 2 no interpreta JSON correctamente, por lo tanto vamos a tener que hacer algo para hacerlo funcionar.

El enfoque más común sería hacer un Custom Model Binder. Sin embargo, esto implica que en cada acción que recibe JSON, debemos indicar explícitamente a MVC que utilice nuestro Model Binder. Si se trata de una sola acción, no habría ningún problema, pero si hay muchas acciones que utilizan el Model Binder, y todas reciben distintos tipos de datos, se va a volver muy tediosa la implementación.

Por suerte, en MVC 2, tenemos algo llamado Value Providers. Mientras que los Model Binders son utilizados para bindear datos que recibe el servidor, Value Providers proveen una abstracción para los datos en sí.

Para resolver mi problema, creé un Custom Value Provider que recibe datos JSON y lo serializa a un diccionario, en lugar de al objeto. Luego, éste diccionario es pasado al Default Model Binder de MVC, el cual realiza el bind al objeto final, e incluso realiza cualquier validación que ustedes hayan indicado.

El Custom Value Provider es el siguiente:

   1:  public class JsonValueProviderFactory : ValueProviderFactory
   2:  {
   3:      public override IValueProvider GetValueProvider
   4:                                           (ControllerContext controllerContext)
   5:      {
   6:          object jsonData = GetDeserializedJson(controllerContext);
   7:   
   8:          if (jsonData == null)
   9:              return null;
  10:   
  11:          var dictionary = new Dictionary<string, object>
  12:                                             (StringComparer.OrdinalIgnoreCase);
  13:   
  14:          FlattenToDictionary(dictionary, string.Empty, jsonData);
  15:   
  16:          return new DictionaryValueProvider<object>
  17:                                       (dictionary, CultureInfo.CurrentCulture);
  18:      }
  19:   
  20:      private static object GetDeserializedJson
  21:                                           (ControllerContext controllerContext)
  22:      {
  23:          if (!controllerContext.HttpContext.Request.ContentType
  24:            .StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
  25:              return null;
  26:   
  27:          string bodyText = new StreamReader
  28:                (controllerContext.HttpContext.Request.InputStream).ReadToEnd();
  29:   
  30:          if (string.IsNullOrEmpty(bodyText))
  31:              return null;
  32:   
  33:          var serializer = new JavaScriptSerializer();
  34:   
  35:          return serializer.DeserializeObject(bodyText); 
  36:      }
  37:   
  38:      private static void FlattenToDictionary(
  39:            IDictionary<string, object> dictionary, string prefix, object value)
  40:      {
  41:          var dictionaryValue = value as IDictionary<string, object>;
  42:   
  43:          if (dictionaryValue != null)
  44:          {
  45:              foreach (KeyValuePair<string, object> entry in dictionaryValue)
  46:              {
  47:                  string propertyKey;
  48:   
  49:                  if (!string.IsNullOrEmpty(prefix))
  50:                      propertyKey = prefix + "." + entry.Key;
  51:                  else
  52:                      propertyKey = entry.Key;
  53:   
  54:                  FlattenToDictionary(dictionary, propertyKey, entry.Value);
  55:              }
  56:          }
  57:          else
  58:          {
  59:              var listValue = value as IList;
  60:              if (listValue != null)
  61:              {
  62:                  for (int i = 0; i < listValue.Count; i++)
  63:                      FlattenToDictionary(dictionary,
  64:                                          prefix + "[" + i + "]", listValue[i]);
  65:              }
  66:              else
  67:                  dictionary[prefix] = value;
  68:          }
  69:      }
  70:  }

Y luego, debemos agregar la siguiente línea en el Global.ajax de nuestra aplicación:

   1:  protected void Application_Start()
   2:  {
   3:          RegisterRoutes(RouteTable.Routes);
   4:          ValueProviderFactories.Factories.Add(new JsonValueProviderFactory());
   5:  }

Listo! Ahora al enviar JSON al servidor, nuestro Value Provider interceptará el request, y serializará los datos de tipo JSON a un diccionario que pueda ser interpretado por el Default Model Binder. Lo único que debemos recordar hacer es setear el ContentType a 'application/json', y éstos requests serán procesados por nuestro Value Provider.

Ahora, veamos un poco qué está haciendo el Value Provider.
Al único método que tenemos que hacerle override de la clase ValueProviderFactory es a GetValueProvider(). Lo que hace es deserializar JSON y transformarlo en un objeto .NET, luego lo serializa al diccionario y devuelve un DictionaryValueProvider, el cual es utilizado más tarde por el default model binder.

El método GetDeserializedJson() hace exactamente lo que dice, deserializa JSON. Hay que tener cuidado de pasarle un string que contenga JSON, porque puede arrojar una excepción si se pasa JSON invalido. Probablemente sería conveniente encerrar a la llamada DeserializeObject en un try/catch, y devolver null cuando falle.

Finalmente, el método FlattenToDictionary() es un método recursivo que recorre el objeto deserializado, que para este ejemplo es el siguiente:

clip_image002

Como verán, el objeto deserializado es un Dictionary<string, object>. Para Id y Nombre, el tipo de dato de Value es int y string respectivamente. Para Promociones, en cambio, es un Array de object, y cada elemento de ese Array es un Dictionary<string,object>. Lo que hace FlattenToDictionary() es recorrer todos estos elementos para 'aplanarlo' (flatten) a un diccionario. El método nos devuelve esto:

clip_image004

Si leyeron el post de Andres Stang probablemente esto les resulte familiar. Lo que hace el método es recorrer el objeto, y construir el Key de cada uno de los valores que contiene para poder insertarlo en un diccionario, donde todos los valores de cada elemento sea un tipo de dato primitivo. Este diccionario luego es interpretado por el default model binder. Es decir, lo que estamos haciendo es transformar JSON a un formato que pueda ser interpretado por el default model binder.

Les dejo el código fuente con una aplicación de ejemplo para que prueben:

JsonToMVCAction.rar (153,28 kb)

Tags: , , ,

.NET | ASP.NET | Desarrollo Web | JQuery

Lanzamiento Virtual “La eficiencia en tus manos”

por Daniel Laco  16. abril 2010

Hola a todos, quiero recordarles que este 21 de Abril será el evento virtual de lanzamiento de 11 importantes productos y donde muchos MVPs estaran como speakers y como expertos para resolver tus dudas.

clip_image003_010b7c0b-5450-4bae-8599-79e3ef3ca26b

 clip_image001_0d130959-9982-4736-8319-5fff7ce34ca4

 

 

clip_image002_f403aaf7-c2c3-4e8d-beb7-f7bb8c027891

Regístrate ya!

Tags:

.NET | ASP.NET | Entity Framework | General | Sharepoint | Visual Studio

Que hay de nuevo en VS2010?

por Daniel Laco  4. marzo 2010

Ya se viene la liberación de la versión final de Visual Studio 2010 y .NET 4.0 y uno siempre quiere tener una vista rápida de los cambios, agregados, etc.

En http://msdn.microsoft.com/en-us/library/bb386063(VS.100).aspx pueden encontrar un listado de todos los temas nuevos de esta plataforma.

 

Tags: , , , ,

.NET | ADO.NET | ASP.NET | Entity Framework | Visual Studio | WCF | WinForms

Acerca de los Autores

Este es el blog del equipo de VEMN SA 
Presentaremos temas que nos parezcan de interés sobre tecnología .NET, Procesos y Metodologías y todo aquello relacionado con el proceso de desarrollo de Software

Month List

BlogRoll

Download OPML file OPML