Una nueva solución para un viejo problema de almacenamiento del estado

MSDN Magazine, abril de 2006

Publicado: 16 de Mayo de 2006

Fritz Onion

Descargar el código de este artículo: ExtremeASPNET0604.exe (120 KB)

Resumen: Extreme ASP.NET: Una nueva solución para un viejo problema de almacenamiento del estado

En esta página
IntroducciónIntroducción
Conceptos fundamentales sobre ProfileConceptos fundamentales sobre Profile
SerializaciónSerialización
Tipos definidos por el usuario como propiedades de ProfileTipos definidos por el usuario como propiedades de Profile
Optimización de ProfileOptimización de Profile
Optando por personalizarOptando por personalizar
ConclusiónConclusión
Acerca del autorAcerca del autor
*

Introducción

La administración del estado de las aplicaciones Web es un tema muy polémico. ¿Deberían almacenarse los datos de usuario por sesión o deben persistir a lo largo de diferentes sesiones? El estado de la sesión permite almacenar temporalmente la información de forma sencilla mientras un usuario navega por el sitio. Normalmente, se utiliza un almacén de datos en la memoria, que está indizado por una clave de sesión única asignada a cada nuevo usuario que perdura únicamente lo que dura la "sesión" con el cliente. El almacenamiento de información a lo largo de varias sesiones normalmente se consigue creando un almacén de datos de servidor indizado por algún identificador de usuario, que normalmente se obtiene cuando el usuario inicia la sesión en el sitio.

Pero, entre estos dos niveles de almacenamiento por cliente, existe cierta zona gris. ¿Qué ocurre si desea almacenar también los datos de las sesiones para los usuarios anónimos? ¿No sería cómodo poder almacenar los datos en nombre de los clientes en un almacén persistente sin tener que implementar un almacén completo de datos de servidor? A estas necesidades responde precisamente la característica Profile de ASP.NET 2.0.

Profile ofrece una manera sencilla de definir información de perfil de usuario con copia en una base de datos. Con unas pocas entradas de archivo de configuración, puede crear rápidamente un sitio que almacene las preferencias del usuario (o cualquier otro dato de este tipo) en una base de datos, todo ello con una sencilla interfaz con seguridad de tipos para el programador. El aspecto y manejo de Profile es muy similar al estado de la sesión, pero con la diferencia de que persiste a lo largo de diferentes sesiones. Profile está asociado al sistema de pertenencia de ASP.NET 2.0, de manera que los datos de los clientes autenticados se almacenan asociándolos a sus identidades reales en vez de a un identificador arbitrario. Para los clientes anónimos se genera un identificador que se almacena como una cookie persistente, por lo que los accesos posteriores que se realicen desde el mismo equipo conservarán las preferencias.

Para los programadores de ASP.NET es importante comprender las opciones disponibles para almacenar el estado y las situaciones a las que mejor se adaptan cada una de estas opciones. En esta columna examinaremos Profile y la posición que ocupa dentro del abanico de opciones de administración del estado.

Principio de la páginaPrincipio de la página

Conceptos fundamentales sobre Profile

El primer paso para utilizar Profile consiste en declarar las propiedades que queremos almacenar en nombre de cada usuario en el archivo Web.config mediante el elemento <profile>. En el ejemplo de la figura 1 se muestran tres declaraciones de propiedades, cada una para el color favorito, el número favorito y el código de estado de HTTP favorito del usuario.

Cuando ASP.NET compila el sitio, crea una nueva clase derivada de ProfileBase con los descriptores de acceso con seguridad de tipos para las propiedades declaradas. Estos descriptores de acceso utilizan el proveedor de profile para guardar y recuperar estas propiedades interactuando con la base de datos para la que se haya configurado el proveedor. La figura 2 muestra el aspecto que presenta la clase generada para las propiedades declaradas en la figura 1.

A continuación, ASP.NET agrega una declaración de propiedad para una propiedad llamada Profile para cada clase derivada de Page del sitio. Se trata de un descriptor de acceso con seguridad de tipos para la clase Profile actual (que forma parte de HttpContext):

public partial class Default_aspx : Page
{
   protected ProfileCommon Profile {
      get { return ((ProfileCommon)(this.Context.Profile)); }
   }

   //...
}

Esto permite interactuar con las propiedades de profile de una manera muy cómoda. Por ejemplo, a continuación vemos un fragmento de código que establece las propiedades de profile en función de los campos de un formulario:

void enterButton_Click(object sender, EventArgs e)
{
  Profile.FavoriteColor = colorTextBox.Text;
  Profile.FavoriteNumber = int.Parse(numberTextBox.Text);
  Profile.FavoriteHttpStatusCode = 
      Enum.Parse(typeof(HttpStatusCode), statusCodeTextBox.Text);
}

Si observa la base de datos que utiliza el proveedor de profile (de manera predeterminada, una base de datos local de SQL Server™ 2005 Express en el directorio App_Data de la aplicación), verá una tabla llamada aspnet_Profile con cinco columnas con los siguientes nombres:

UserId
PropertyNames
PropertyValuesString
PropertyValuesBinary
LastUpdatedDate

Para el ejemplo anterior, estas columnas se rellenaron con los siguientes valores:

405A7333-2C8D-4E63-AB56-BA54398D47DF
FavoriteColor:S:0:3:FavoriteNumber:S:3:2:FavoriteHttpStatusCo
de:S:5:16:
red42MovedPermanently
<empty>
2006-1-1 09:00:00.000

Puede ver que, de manera predeterminada, el proveedor de profile utiliza la serialización de cadenas con nombres de propiedades (y longitudes de cadena), almacenadas cuidadosamente para cada usuario. En este ejemplo el usuario era anónimo, por lo que se generó un GUID que se utilizó para indizar los valores de las propiedades en la tabla aspnet_Profile. La columna UserId es en realidad una referencia de clave externa a la columna UserId de la tabla aspnet_Users, en la que el sistema de pertenencia mantiene la información sobre los usuarios (los usuarios anónimos también se almacenan en esta tabla).

Principio de la páginaPrincipio de la página

Serialización

Como hemos visto, el método de serialización predeterminado para las propiedades almacenadas en Profile es escribirlas como cadenas, almacenando los nombres de las propiedades y los índices de subcadenas en la columna PropertyNames. Puede controlar cómo se serializan las propiedades si cambia el atributo serializeAs del elemento add de Web.config. Estos atributos sólo pueden tener uno de estos cuatro valores: Binary, ProviderSpecific, String o Xml.

El valor predeterminado es ProviderSpecific, al que sería más apropiado llamar TypeSpecific, ya que el tipo del objeto determinará el formato de la serialización. Con la implementación predeterminada de SQL Provider, ProviderSpecific indica que la propiedad se escribirá como una cadena simple si ya es una cadena o un tipo primitivo (int, double, float, etc.). Para los demás casos, utiliza de forma predeterminada la serialización XML, algo lógico teniendo en cuenta que funcionará con la mayoría de los tipos (incluso los personalizados) sin tener que realizar ninguna modificación a la propia definición de tipo. Por tanto, ProviderSpecific podría haberse llamado StringForPrimitiveTypesAndStringsOtherwiseXml, pero al ser tan largo, fue necesario acortarlo.

Esto podría llevar a cierto comportamiento confuso si no se tiene cuidado. Por ejemplo, imaginemos que tenemos estas dos definiciones de la propiedad Profile:

<add name="TestCode" type="System.Net.HttpStatusCode" 
     defaultValue="OK" />
<add name="TestDate" type="DateTime" 
     defaultValue="1/1/2006"/>

Tras utilizar las propiedades de profile integer y string, parece razonable agregar una enum y una DateTime de esta manera. Como la serialización predeterminada es ProviderSpecific, debe saber que estos dos tipos se serializarán con el XmlSerializer, por lo que no será posible especificar valores predeterminados como cadenas simples (tal como descubrirá rápidamente en cuanto intente obtener acceso a las propiedades). Hay dos maneras de solucionar este problema. Una consiste en especificar el valor serializado en XML directamente en el archivo de configuración (teniendo cuidado de escribir todos los corchetes angulares con los códigos adecuados):

<add name="TestCode" type="System.Net.HttpStatusCode" 
     defaultValue="&lt;HttpStatusCode&gt;OK&lt;/HttpStatusCode&gt;" />
<add name="TestDate" type="DateTime" 
     defaultValue="&lt;dateTime&gt;2006-01-01&lt;/dateTime&gt;" />

La otra opción (tal vez más atractiva) es cambiar la serialización de ProviderSpecific (que como sabe utiliza XML) a String. La serialización String sólo funciona para los tipos que tienen TypeConversions definidas para cadenas, lo que nos sirve aquí, ya que las enumeraciones y la clase DateTime tienen conversiones a cadena definidas. (En la siguiente sección veremos cómo escribir sus propias conversiones a cadena.) Si examina cuidadosamente la figura 1, observará que especifica un atributo serializeAs de String para FavoriteHttpStatusCode, por lo que sería posible utilizar un simple valor predeterminado de cadena "OK":

<add name="TestCode" type="System.Net.HttpStatusCode" serializeAs="String" defaultValue="OK" /><add name="TestDate" type="DateTime" serializeAs="String" defaultValue="2006-01-01" />

La otra opción de serialización es Binary, que utilizará BinaryFormatter para serializar la propiedad y, con el proveedor de SQL predeterminado, escribirá los datos binarios en la columna PropertyValuesBinary de la base de datos. Esta opción resulta útil si desea dificultar la lectura directa de los valores de profile de la base de datos o si quiere almacenar tipos cuyo estado completo no persiste correctamente mediante XmlSerializer (las clases con miembros de datos privados a los que no se puede obtener acceso mediante las propiedades públicas caen dentro de esta categoría, por ejemplo). Antes de utilizar la opción binary, el tipo que se está almacenando debe marcarse como Serializable o debe implementar la interfaz ISerializable. No olvide que la serialización binaria dificulta la especificación de un valor predeterminado, por lo que suele utilizarse únicamente en tipos complejos para los que, de todas maneras, no tiene sentido establecer un valor predeterminado. Si necesita especificar un valor predeterminado para la serialización binaria, puede aplicarla a la instancia que desea utilizar como valor predeterminado, codificar en base64 la matriz de bytes resultante y utilizar esta representación con codificación base64 como valor del atributo defaultValue.

Principio de la páginaPrincipio de la página

Tipos definidos por el usuario como propiedades de Profile

Una de las ventajas de la arquitectura de Profile es que resulta lo suficientemente genérica como para almacenar cualquier tipo y, tal como hemos visto, admite varios modelos diferentes de persistencia, por lo que es bastante sencillo escribir sus propias clases para almacenar datos del usuario y almacenar toda la clase en Profile. Imaginemos que desea proporcionar un carro de la compra a los usuarios. Puede escribir una clase para almacenar un artículo individual que contenga una descripción y un precio, y otra clase que mantenga una lista de todos los artículos presentes en el carro actual y, además, exponga una propiedad que calcule el precio total de todos los artículos del carro (consulte la figura 3).

Observe que estas clases se marcan con el atributo [Serializable] para preparar de antemano el uso de la serialización binaria. A continuación, puede agregar una propiedad profile de tipo ShoppingCart a la colección y conseguir implementar así un carro de la compra persistente por cliente con copia en una base de datos.

<profile enabled="true">
  <properties>
    <add name="ShoppingCart" type="MsdnMag.ShoppingCart" 
         allowAnonymous="true" serializeAs="Binary" />
  </properties>
</profile>

Para utilizar el carro de la compra en la aplicación, bastará con consultar la propiedad ShoppingCart de Profile y agregar nuevas instancias de la clase Item según sea necesario (el ejemplo que puede descargar incluye una interfaz completa para que los usuarios puedan hacer sus compras utilizando esta clase como mecanismo de almacenamiento).

Profile.ShoppingCart.Items.Add(
    new Item("Chocolate covered cherries", 3.95F));
Principio de la páginaPrincipio de la página

Optimización de Profile

Tal vez en este momento se esté preguntando cuántos recursos se necesitan al utilizar Profile para almacenar los datos por clientes. Concretamente, si comienza a utilizar clases complejas como ShoppingCart es posible que acabe almacenando cantidades ingentes de información en nombre de cada usuario. Tal vez desconfíen especialmente todos aquellos que hayan utilizado la característica de estado de la sesión con copia en SQL Server que se incluyó en ASP.NET 1.0 ya que, de manera predeterminada, cada solicitud de una página suponía dos viajes de ida y vuelta a la base de datos de estado para enviar el estado de la sesión a la base de datos y recuperarlo de ella. La buena noticia es que, de manera predeterminada, el mecanismo de persistencia mediante Profile es razonablemente eficiente. A diferencia del estado de la sesión fuera de proceso, realiza una recuperación diferida de los datos de perfil en nombre de un usuario (que se carga sólo si se solicita) y, a continuación, vuelve a escribir los datos de profile sólo si cambian.

Desgraciadamente, si almacena algo que no sean cadenas, como clases DateTime o tipos primitivos, ProfileModule no podrá determinar si el contenido ha cambiado realmente, por lo que se ve obligado a escribir profile en la base de datos cada vez que se recupera. Como esto también se aplica a las clases personalizadas, tenga en cuenta que al agregar cualquier tipo que no sea una cadena, DateTime o un tipo primitivo, Profile se verá obligado a escribir en la base de datos al final de cada solicitud que solicite el acceso a Profile. Internamente, hay un indicador de obsolescencia que realiza un seguimiento de si ha cambiado una propiedad de Profile. Puede establecer explícitamente la propiedad IsDirty de una propiedad de profile en false. Si se hace así para todas las propiedades asociadas con una instancia de un proveedor concreto, cuando se pregunte a dicha instancia del proveedor si se guardan los datos de perfil, verá que todas las propiedades que se han pasado no son obsoletas y omitirá la comunicación con la base de datos. Este método se basa en el conocimiento de los tipos SettingsBase, SettingsProperty y SettingsPropertyValue subyacentes (todos ellos en System.Configuration). Para una propiedad de profile llamada "Nickname", podría obligar a que no se considerase obsoleta mediante un código como el siguiente:

Profile.PropertyValues["Nickname"].IsDirty = false;

Observe que puede desactivar el almacenamiento automático de profile utilizando el atributo automaticSaveEnabled del elemento <profile/> del archivo de configuración (este atributo toma de manera predeterminada el valor true). Puede establecer automaticSaveEnabled en false para evitar que ProfileModule almacene automáticamente Profile en su nombre. Si lo desea, puede llamar a Profile.Save si desea almacenar los datos en la base de datos. Otra posibilidad consiste en enlazar con el evento ProfileAutoSaving de ProfileModule. Si establece la propiedad ContinueWithProfileAutoSave del argumento event en false, ProfileModule no llamará a Profile.Save.

Como hemos visto anteriormente, es posible especificar String, Binary o Xml como mecanismo de serialización de las propiedades. Si desea almacenar sus propias clases personalizadas, como ocurre en el ejemplo ShoppingCart, hay dos maneras de reducir el espacio utilizado para almacenar las instancias de la clase: escribir su propio TypeConverter para que la clase admita la conversión al formato cadena o implementar la interfaz ISerializable para controlar el formato de los datos binarios que utiliza BinaryFormatter. La figura 4 muestra la serialización predeterminada de la clase ShoppingCart con cuatro elementos en ella, en XML.

De manera predeterminada, no puede utilizar la opción serializeAs="String" para los tipos personalizados, ya que es posible realizar las conversiones entre el tipo y el formato de cadena sin que se produzcan pérdidas. Puede proporcionar tal conversión personalmente si implementa un TypeConverter para la clase, lo que implica crear una clase que herede de TypeConverter, implementar los métodos de conversión y anotar la clase original con el atributo TypeConverter que lo asocia con la clase de conversión. Debe decidir cómo realizar la persistencia de la clase como una cadena (y también cómo analizarla para extraerla de una cadena), lo que puede ser una tarea compleja, por lo que es conveniente de que se asegure de que el esfuerzo merece la pena antes de ponerse manos a la obra. Como ejemplo, la figura 5 muestra una clase TypeConverter para la clase Item que representa los artículos del carro de la compra. En este caso, he optado por utilizar un carácter no imprimible como delimitador. Como la clase Item consta de dos trozos de estado que pueden representarse fácilmente como cadenas, para realizar el análisis basta con utilizar el método Split de la clase string. A continuación, se asocia la clase converter con la clase Item utilizando el atributo TypeConverter.

Tras crear estas clases, se puede utilizar la clase Item con la serialización de cadena en una propiedad de profile. Para que sea posible serializar completamente el carro de la compra como una cadena, debe escribir también un convertidor de tipo para la clase ShoppingCart (puede encontrar un ejemplo si descarga los ejemplos que se incluyen en este artículo). La ventaja de controlar la persistencia de esta manera es que la serialización del mismo carro de la compra con cuatro artículos ahora sólo ocupa 79 caracteres

Principio de la páginaPrincipio de la página

Optando por personalizar

Cuando hay que dedicar mucho tiempo a conseguir que una arquitectura haga lo que uno desea, cabe preguntarse si todo este esfuerzo es menor que el que supondría hacerlo todo por uno mismo. Profile es un excelente ejemplo de una característica cómoda y fácil de utilizar, pero que puede resultar demasiado restrictiva a medida que evolucione el diseño. Vamos a ver las características concretas que nos ofrece Profile.

Clientes autenticados y anónimos

Usuarios anónimos identificados mediante una nueva cookie (o también mediante un Id. incrustado en la dirección URL, incluida la compatibilidad con la detección automática del modo sin cookies)

Almacenamiento de cualquier tipo, con establecimiento inflexible de tipos mediante el uso de un archivo de configuración

Un almacén de datos persistente por cliente

Una clase de administración para la limpieza de datos de perfiles no utilizados

Uno de los inconvenientes de utilizar Profile para almacenar los datos del cliente es que todos los datos se almacenan en una columna de la tabla de la base de datos (o en dos si se utiliza la serialización de cadena y binaria), lo que hace prácticamente imposible realizar modificaciones en los datos de profile sin recurrir a la API de Profile. Tampoco resulta práctico generar ningún informe a partir de los datos o recopilar directamente la información de la base de datos de ninguna otra manera.

Si desea tener un mayor control sobre el almacenamiento del estado por cliente en la aplicación, tiene dos opciones: crear un proveedor de profile personalizado o bien olvidarse de Profile y escribir los datos personalmente. La creación de un proveedor personalizado de profile le ofrece la posibilidad de cambiar la ubicación en la que Profile escribe los datos, pero debido a la naturaleza de la interfaz del proveedor, no simplifica en realidad la escritura de los valores de las propiedades en determinadas columnas de una tabla. Para obtener más información y ejemplos sobre la creación de proveedores personalizados de profile, échele un vistazo a ASP.NET provider model toolkit (en inglés). También puede consultar ASP.NET 2.0 Table Profile Provider sample (en inglés).

Si decide olvidarse de Profile y escribir a su manera la serialización de los datos del cliente, recuerde que puede seguir aprovechando las características de identificación de los perfiles aunque no utilice las características de almacenamiento. Concretamente, hay una propiedad UserName en la clase ProfileBase que contiene el nombre del usuario autenticado en ese momento o el GUID generado para un usuario anónimo. Puede utilizar esta propiedad UserName como índice único de una tabla de base de datos personalizada para almacenar y recuperar fácilmente los datos de los usuarios. Sólo tiene que asegurarse de que se han habilitado Profile y anonymousIdentification en su aplicación, y podrá utilizar el mismo mecanismo de identificación de clientes que Profile:

<anonymousIdentification enabled="true"/>
<profile enabled="true" />

La escritura de su propio servidor de persistencia de clientes utilizando el identificador único que proporciona Profile le ofrece varias ventajas exclusivas:

Posibilidad de escribir procedimientos almacenados para los datos de los clientes.

Posibilidad de recuperar únicamente la parte de los datos que necesita para un cliente en cualquier momento en vez de depender de que Profile sencillamente cargue el fragmento completo en la memoria. Tenga en cuenta, no obstante, que cada elemento <add /> de la propiedad en el archivo de configuración incluye un atributo provider, que le permite dividir las propiedades de Profile entre varios proveedores. Por ejemplo, puede asignar las propiedades utilizadas con mayor frecuencia a una instancia de proveedor y las propiedades menos frecuentes a otra. Si nunca consulta las propiedades menos utilizadas en una página durante la ejecución, la característica Profile cargará únicamente la información asociada al proveedor de "uso frecuente". Éste es otro ejemplo de cómo aprovechar la característica de carga diferida de Profile.

Posibilidad de almacenar en caché los datos por cliente a través de las solicitudes por eficiencia (puede agregar esto derivando de SqlProfileProvider y omitiendo GetPropertyValues y SetPropertyValues para agregar semántica de almacenamiento en caché por usuario).

Control completo sobre la serialización y posibilidad de asignar a tablas existentes en vez de crear nuevos almacenes de datos que puede estar utilizando ya.

El ejemplo que puede descargar del sitio Web de MSDN®Magazine contiene una implementación alternativa del carro de la compra que se ha descrito anteriormente, utilizando una tabla de base de datos personalizada para almacenar los artículos del carro y aprovechando el identificador de cliente único que ofrece la clase ProfileBase. Otra posibilidad consiste en empezar utilizando Profile para, más adelante, migrar parte de los datos de profile a tablas personalizadas mediante una capa de acceso a datos diferente. En este sentido, utilizar Profile es una manera sencilla de almacenar los datos por cliente, con una ruta evidente para reutilizar los datos en un esquema con un establecimiento más inflexible de tipos.

Principio de la páginaPrincipio de la página

Conclusión

Profile rellena un hueco en la administración del estado de las aplicaciones Web escritas con ASP.NET. Situado en alguna parte entre el estado de la sesión en proceso y un almacén de datos por cliente personalizado completamente desarrollado, Profile resultará extremadamente útil en prácticamente todas las aplicaciones Web. No obstante, tal y como ocurre con todas las soluciones de uso general, deberá renunciar a cierto rendimiento y flexibilidad respecto a una implementación completamente personalizada, por lo que deberá tener en cuenta las posibilidades que ofrece al integrarla en sus diseños.

Envíe sus preguntas y comentarios a Fritz escribiendo a xtrmasp@microsoft.com.

Principio de la páginaPrincipio de la página

Acerca del autor

Fritz Onion es cofundador de Pluralsight, un proveedor de cursos de Microsoft .NET, donde dirige el plan de estudios de desarrollo Web. Fritz es el autor de Essential ASP.NET (Addison Wesley, 2003) y de Essential ASP.NET 2005 (Addison Wesley, 2006), que se publicará próximamente. Puede ponerse en contacto con él en pluralsight.com/fritz.


Principio de la páginaPrincipio de la página