martes, 13 de diciembre de 2011

Introducción a less

Por casualidad, he encontrado less para trabajar con CSS.

Lees en un lenguaje dinámico para trabajar con hojas de estilo que permite incluir variables, funciones y un buen número de características dinámicas, propias de la programación, a nuestras hojas de estilo.

Su funcionamiento es muy sencillo, simplemente utilizamos una sintaxis concreta para especificar un comportamiento dinámico (completamente basada en CSS para no tener que reaprender un nuevo lenguaje) y después y en tiempo de ejecución, todas nuestras instrucciones serán resueltas antes de devolver el código CSS final que el navegador entenderá perfectamente.

Cómo más vale un buen ejemplo que mil palabras, hay va nuestro primer fichero .less

@color-principal: #3879BD;
@color-secundario: #f859Bf;
.esquinas-redondeadas(@radius: 3px) { 
    -webkit-border-radius: @radius;
    -moz-border-radius: @radius;
    border-radius: @radius;
}
#header 
{
    .esquinas-redondeadas; 
	background-color: @color-secundario;   
}
#content 
{
    .esquinas-redondeadas(5px);
    background-color: @color-principal;
}

En este ejemplo:



  • Utilizamos variables con @color-principal y @color-secundario.

  • Utilizamos agrupación de propiedades (mixin) con .esquinas-redondeadas


El código CSS que resulta después de haber hecho su trabajo less es el siguiente:

#header {
    background-color: #F859BF;
    border-radius: 3px 3px 3px 3px;
}
#content {
    background-color: #3879BD;
    border-radius: 5px 5px 5px 5px;
}

La verdad es que me parece todo un invento que intentaré utilizar en futuros desarrollos. Dicho queda.

Se puede utilizar less tanto desde el lado del cliente como del lado del servidor.

Para utilizar less desde el lado del cliente y olvidarnos de qué lenguaje estamos utilizando del lado del servidor:



  • Crear nuestra hoja de estilos con las características less que creamos oportunas.

  • Renombrar la extensión de la hoja de estilos a .less.

  • Incluir la hoja de estilos en nuestra página con la siguiente sintaxis:


    • <link href="Comun.less" rel="stylesheet/less" type="text/css" />

  • Descargar la última versión del fichero .js desde la página de less. En mi caso es less-1.1.5.min.js.



    • Incluir una referencia a nuestro fichero .js después de las hojas de estilo.


Y ya está, el fichero .js hará magia y todas las hojas de estilo con la extensión .less y el atributo rel igual a “stylesheet/less” serán procesados y convertidos a CSS convencional.

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <link href="Comun.less" rel="stylesheet/less" type="text/css" />
    <script src="less-1.1.5.min.js" type="text/javascript"></script>
</head>
<body>
    <div id="header">
        Cabecera
    </div>
    <div id="content">
        Contenido
    </div>
</body>
</html>

Por otro lado, si trabajamos con ASP.NET también podemos utilizar less desde el lado del servidor y aprovecharnos de otras características extras como la minificación o cacheo de nuestras hojas de estilo.

La implementación de less en ASP.NET la puedes encontrar en dotlesscss.org. Lo que hace dotlesscss es agregar un HTTP Handler que procesará automáticamente todas las peticiones para ficheros .less. De este modo, el pre-procesamiento de estos ficheros se hará en el servidor y ya no será necesario el fichero .js que utilizamos desde el lado cliente.

Para hacer funcionar less desde el lado del servidor lo más fácil es utilizar NuGet e instalar el paquete dotless que agregará el ensamblado dotless.Core.dll y toda la información necesaria en nuestro fichero web.config.

Un ejemplo de cómo quedaría el web.config después de registrar el manejador, una sección personalizada, etc:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="dotless" type="dotless.Core.configuration.DotlessConfigurationSectionHandler, dotless.Core" />
  </configSections>
  <system.web>
    <compilation debug="true" strict="false" explicit="true" targetFramework="4.0" />
    <httpHandlers>
      <add path="*.less" verb="GET" type="dotless.Core.LessCssHttpHandler, dotless.Core" />
    </httpHandlers>
  </system.web>
  <dotless minifyCss="false" cache="true" web="false" />
  <system.webServer>    
    <handlers>
      <add name="dotless" path="*.less" verb="GET" type="dotless.Core.LessCssHttpHandler,dotless.Core" resourceType="File" preCondition="" />
    </handlers>
  </system.webServer>
</configuration>

A partir de aquí, el mismo ejemplo anterior funciona exactamente igual desde el lado del servidor.

Fíjate además cómo ahora podemos incluso minificar el CSS obtenido si activamos el parámetro minifyCss:

#header{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;background-color:#f859bf;}#content{-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;background-color:#3879bd;}

Si utilizamos dotless, tenemos además otras ventajas:



  • Podemos compilar automáticamente nuestro ficheros .less durante el build de nuestra aplicación y así obtener directamente el código CSS sin tener que agregar un manejador HTTP.

  • Podemos utilizar funciones de dotlless directamente desde nuestro código sin tener que estar en el contexto de un fichero .less


Puedes encontrar documentación sobre dotless en el siguiente enlace https://github.com/dotless/dotless/wiki

Aunque hoy sólo hemos visto las variables y el mixing, te adelanto que less son muchas más cosas como operaciones, funciones, espacios de nombres, etc.

Por cierto, no dejes de leer el siguiente enlace de nuestro amiguito Scott Mitchell que lo explica perfectamente http://www.4guysfromrolla.com/articles/030310-1.aspx o también este artículo de Gisela Torres que está muy bien http://www.returngis.net/2013/03/less-preprocesador-css-con-soporte-en-visual-studio-2012/

Yo le voy a dar una oportunidad ¿Y tú?

Un saludo!

lunes, 12 de diciembre de 2011

Entity Framework, DefiningQuery ¿Virtud o defecto?

En anteriores post vimos los distintos tipos de asociaciones disponibles, así como distintas soluciones para implementar la herencia.

Ahora nos toca ver como trabajar con vistas en nuestro modelo EDM.

Para llevar a cabo los ejemplos de este post, partimos de una base de datos una vista llamada DireccionesPorCliente (una simple INNER JOIN entre tablas Cliente y Direcciones)

Al crear nuestro modelo con Database First (esto es primero la base de datos y después generar el modelo con el asistente) podemos observar que se ha creado una entidad llamada “DireccionesPorCliente” donde todos sus campos son clave. Esto es así porque cualquier entidad en EF tiene que tener al menos un campo clave y, al no encontrar EF ningún campo candidato, toma la determinación de que todos los campos serán clave. Lógicamente somos libres de modificar la entidad para que sólo un campo o varios sean clave. En realidad y puesto que la entidad resultante de la vista será de sólo-lectura, es simplemente una cuestión de rendimiento y/o comodidad el dejar que todos los campos sean clave.

Por otro lado, la sección SSDL (la sección de nuestro fichero edmx que define la estructura de la base de datos) ha incluido un elemento DefiningQuery que incluye un comando SQL nativo para obtener los datos de la vista. Esto significa que las vistas que se basan en DefiningQuery no podrán ser migradas automáticamente si cambiamos el gestor de base de datos de nuestro modelo, puesto que están atadas a un gestor concreto y su lenguaje SQL específico.

<EntitySet Name="DireccionesPorCliente" EntityType="EFModel.Store.DireccionesPorCliente" store:Type="Views" store:Schema="dbo" store:Name="DireccionesPorCliente">   
<DefiningQuery>
SELECT       
[DireccionesPorCliente].[IdCliente] AS [IdCliente],       
[DireccionesPorCliente].[Nombre] AS [Nombre],       
[DireccionesPorCliente].[IdDireccion] AS [IdDireccion],       
[DireccionesPorCliente].[Direccion] AS [Direccion],       
[DireccionesPorCliente].[Poblacion] AS [Poblacion],       
[DireccionesPorCliente].[CP] AS [CP]      
FROM [dbo].[DireccionesPorCliente] AS [DireccionesPorCliente]
</DefiningQuery>
</EntitySet>

Lo cierto es que según como se mire, no todo son desventajas con los elementos DefiningQuery.


Por ejemplo, el mayor defecto de DefiningQuery también podría ser su mayor virtud. Podríamos beneficiarnos de que guarda el comando SQL en el lenguaje nativo de la base de datos para construir nuestras propias DefiningQuery sin la necesidad de que exista una vista en la base de datos subyacente, y además todo ello con la flexibilidad de poder ejecutar cualquier código SQL que sea válido para el gestor de base de datos (imagino que no quieres dar al traste con todos tus años de experiencia y conocimiento en el lenguaje SQL de tu gestor de base de datos favorito, ¿no?)


En cualquier caso, lo que sí está claro es que el elemento DefiningQuery supondrá series dificultades si queremos migrar nuestro modelo EDM a un gestor de base de datos distinto del actual, pero si no es el caso, bienvenidas sean y podrían ayudarnos a resolver ciertos escenarios.


Imagina ahora que tienes una super-SQL que recupera cierta información de la que quieres disponer en tu modelo a través de una entidad. Asumiendo que no sabemos o no queremos utilizar eSQL, LINQ o un procedimiento almacenado, parece que sólo nos queda la vía del elemento DefiningQuery.


Para crear nuestros propios elementos DefiningQuery necesario editar manualmente el fichero .edmx. Si quieres conocer los pasos necesarios visita el siguiente enlace de MSDN.


A grandes rasgos, lo que se hace es agregar el XML necesario en las secciones SSDL, CSDL y MSL en nuestro fichero edmx. De hecho, el XML agregado es exactamente igual al que podría haber generado el asistente de EF si importamos una vista con la excepción del elemento EntitySet de la sección SSDL.


El elemento DefiningQuery que importamos desde la base de datos (luego está ligado a una vista en la base de datos subyacente) es:


<EntitySet Name="DireccionesPorCliente" EntityType="EFModel.Store.DireccionesPorCliente" store:Type="Views" store:Schema="dbo" store:Name="DireccionesPorCliente">


Por otro lado, nuestra vista personalizada sin ningún tipo de atadura con una vista en la base de datos subyacente es igual pero sin las propiedades store.Type, store:Schema y store:Name. Es decir, todas las propiedades que ligan el elemento de nuestro modelo a un elemento de base de datos se han eliminado.


<EntitySet Name="DireccionesPorCliente" EntityType="EFModel.Store.DireccionesPorCliente">


Qué conste que yo no recomiendo usar DefiningQuery porque está claro que rompe con el patrón de ignorancia de la persistencia, pero al menos hay saber que posibilidades tenemos con este elemento y después cada uno que haga lo que crea oportuno.


Además, en el momento en que actualices el modelo, las secciones personalizadas SSDL y MSL será eliminadas por lo que vuelta a empezar.


Por último, casi todo lo expuesto en este post es igualmente aplicable al elemento QueryView.


Una QueryView es una vista sobre el modelo conceptual, es decir, no se basa en una vista de la base de datos subyacente, sino que a través de eSQL expresa una consulta sobre las entidades de nuestro modelo.


Igualmente un QueryView no tiene soporte en el diseñador y hay que editar manualmente el fichero edmx.


Cabe mencionar que tanto con DefiningQuery como con QueryView, las entidades resultantes son de sólo lectura.


Un saludo!

miércoles, 30 de noviembre de 2011

De vuelta a lo básico, codificar html, javascript y url.

Aunque puede resultar algo obvio a muchos, hoy quiero hablar sobre algo que de vez en cuando me ha dado más de un problema. Estoy hablando de estos maravillosos códigos de producto (o de cliente o de lo que sea) que puede incluir caracteres no habituales como la comilla simple, comilla doble, etc.

Imaginemos que tenemos un producto con un identificador que incluye tanto una comilla simple como una comilla doble, por ejemplo, a partir de ahora nuestro código maldito será “sergio’s” (incluyendo las 2 comillas dobles y la comilla simple).

Aunque lo iremos viendo poco a poco, te adelanto que en este post intentaré resolver como trabajar con este código en HTML, Javascript y como QueryString en una Url.

Si no nos metemos en ningún jardín y utilizamos ASP.NET para renderizar nuestro código HTML, lo cierto es que no tendremos ningún problema. Veamos un ejemplo sencillo donde se vuelca nuestro  código en una caja de texto.

El código de servidor ASP.NET es el siguiente:

txtIdProducto.Text = """sergio's"""


Y el HTML generado es:

<input name="txtIdProducto" type="text" value="&quot;sergio&#39;s&quot;" id="txtIdProducto" />

Si nos centramos en el atributo value, podemos ver como ciertos caracteres han sido sustituidos por nombres de entidad HTML:

·         La comilla doble pasó a ser &quot;

·         La comilla simple pasó a ser &#39;

Esto ha sucedido porque cuando se ha renderizado nuestra caja de texto se ha llamado automáticamente al método Server.HtmlEncode para su atributo value.

Es decir, Server.HtmlEncode("""sergio's""") devuelve &quot;sergio&#39;s&quot;

Y por el contrario, Server.HtmlDecode("&quot;sergio&#39;s&quot;") devuelve "sergio's"

La referencia de las entidades HTML (ya sea con nombre o numérico) las puedes encontrar aquí, aunque una simple búsqueda en google te arrojará decenas de resultados.

Para que te hagas una idea y veas que cualquier carácter puede ser representado con una entidad HTML, podríamos escribir lo siguiente (que es totalmente ilegible pero es igualmente válido):

<input type="text" value="&quot;&#115;&#101;&#114;&#103;&#105;&#111;&#39;&#115;&quot;" />

En este ejemplo, además de las comillas dobles y la comilla simple, hemos sustituido el resto de caracteres por su entidad numérico HTML:

·         . &quot;

·         s. &#115;

·         e. &#101;

·         r. &#114;

·         g. &#103;

·         i. &#105;

·         o. &#111;

·         . &#39;

·         s. &#115;

·         . &quot;

Lógicamente, asumo que no te vas a volver loco y a partir de ahora te vas a poner a escribir todo tu código HTML a través de entidades, pero en cualquier caso, podrías.

La conclusión cuando estamos trabajando con código HTML, es que cualquier valor de cualquier atributo de cualquier elemento (¿demasiado cualquier quizás?) tiene que ir acompañado de Server.HtmlEncode. Es decir, imagina que estás construyendo una cadena en servidor con código HTML:

' Mal

' Devuelve <input type='text' value='"sergio's"' />

Dim html As String = String.Format("<input type='text' value='{0}' />", """sergio's""")

 

' Bien

' Devuelve <input type='text' value='&quot;sergio&#39;s&quot;' />

html = String.Format("<input type='text' value='{0}' />", Server.HtmlEncode("""sergio's"""))

 

Aunque sobra decirlo, si recuperamos el valor de nuestro input desde javascript, siempre obtendremos el valor original:

alert(document.getElementById("txtIdProducto").value);

 

clip_image001[4]

Si nos centramos ahora en Javascript, es necesario complicar un poco más nuestro código de producto para que los ejemplos sean más completos. De esta forma, ahora nuestro código de producto pasará a ser “ser\gio’s” (hemos incluido una barra inversa).

Ahora imaginemos que desde ASP.NET queremos generar un saludo con nuestro código de producto al cargar la página. Un posible código sería el siguiente:

Dim idProducto As String = """ser\gio's"""

Dim js As String = String.Format("alert('{0}');", idProducto)

ClientScript.RegisterClientScriptBlock(Me.GetType(), "Page_Load", js)

 

Y la salida que obtenemos es… que no hay alert! Pero bueno, ¿Dónde está mi alert?

Si vemos el código HTML de la página, tiene el siguiente código:

<script type="text/javascript">

//<![CDATA[

alert('"ser\gio's"');//]]>

</script>

Está claro que el mensaje de nuestro alert no está bien construido, así que toca lidiar con ello.

Para esta situación, yo suele escribir mi propia función donde reemplazo ciertos caracteres de la cadena entrante por sus caracteres de escape Javascript:

    Public Shared Function JavascriptEncode(ByVal s As String) As String

        s = s.Replace("\", "\\")

        s = s.Replace("'", "\'")

        s = s.Replace("""", "\""")

        Return s

    End Function

 

·         \ se reemplaza por \\

·         ‘ se reemplaza por \’

·         “ se reemplaza por \”

Como es de esperar, esta función seguro que no contempla ni la mitad de los casos que te pueden ocurrir, pero al menos es un principio. De hecho, le da igual si el delimitador de cadena es la comilla simple o la comilla doble (yo personalmente utilizo indistintamente ambos delimitadores en mi código Javascript cuando trabajo con cadenas).

Si ahora llamamos a la función desde el código de servidor ASP.NET:

Dim idProducto As String = """ser\gio's"""

Dim js As String = String.Format("alert('{0}');", JavascriptEncode(idProducto))

ClientScript.RegisterClientScriptBlock(Me.GetType(), "Page_Load", js)


El HTML resultante ya sí es correcto:

<script type="text/javascript">

//<![CDATA[

alert('\"ser\\gio\'s\"');//]]>

</script>

clip_image002[4]

Por último, hablaremos sobre este tipo de situaciones cuando trabajamos con nuestro código de producto en una Url como parte de la QueryString.

De nuevo, vamos a complicar nuestro código de producto (esta es la última vez, lo juro). Ahora será “&ser\gio’s” (le hemos incluido un ampersand)

Aquí tenemos que contemplar dos escenarios (al menos son con los que yo trabajo habitualmente):

·         Generar la url desde Javascript

·         Generar la url desde ASP.NET, es decir, desde el servidor

Si creamos la url desde Javascript:

var id = document.getElementById("txtIdProducto").value;

var url = "VerProducto.aspx?IdProducto=" + id;

alert(url);

 

clip_image003[4]

 

Está claro que hay no funcionará porque, para empezar, el carácter & es el delimitador de campos en una QueryString y entonces ¿Qué tengo en la url?

clip_image004[4]

Pues resulta que ASP.NET dice tener 2 campos. El segundo campo no tiene nombre y el primer campo no tiene valor… vamos, que la url está muy mal construida.

Para solucionarlo, basta con utilizar la función nativa de Javascript, encodeURIComponent.

var id = document.getElementById("txtIdProducto").value;

var url = "VerProducto.aspx?IdProducto=" + encodeURIComponent(id);

 

clip_image005[4]

clip_image006[4]

Ahora todo funciona correctamente, porque se han sustituido en la url los caracteres reservados por sus equivalentes caracteres de escape.

·         “ por %22

·         & por %26

Ya por último, si nos centramos en la generación de la url desde código de servidor ASP.NET, habrá que utilizar la función Server.UrlEncode.

Dim url As String = "VerProducto.aspx?IdProducto={0}"

url = String.Format(url, Server.UrlEncode("""&sergio's"""))


Que devolverá la cadena:

VerProducto.aspx?IdProducto=%22%26sergio%27s%22

Lo cierto es que este post, da para mucho que escribir, pero al menos espero haber sentado ciertas bases para que los códigos indeseables de productos no te quiten el sueño.

Un saludo!

viernes, 25 de noviembre de 2011

Cómo capturar el tráfico de red en ASP.NET

En nuestra empresa hemos desarrollado una aplicación que queremos comercializar en un modelo Saas y desplegarla en Windows Azure.

Uno de los mayores problemas que nos encontramos en este escenario, es el de conocer qué tráfico de red genera cada usuario, para poder después repercutir ciertos costes de la infraestructura al cliente y empresa adecuada.

Siendo así, hemos pensando en desarrollar una herramienta que sea capaz de capturar el tráfico tanto de entrada como de salida de la aplicación y guardar un registro de esta información.

En principio, cualquiera podría pensar que estamos reinventado la rueda y para que obtener esta información existen los log de servidor de IIS, pero el desarrollo ad-hoc de esta aplicación se sustenta en las siguientes premisas:

  • Si desplegamos nuestra aplicación en Windows Azure, aparentemente no tendremos acceso a los logs de IIS (lo cierto es que no estoy seguro de esta afirmación, pero creo estar en lo cierto). Pues me confirman que sí, sí se puede tener acceso a logs de IIS, tanto en la máquina local accediendo a través de Remote Destkop como transfiriendo los logs al Blob Store (gracias a Carlos Alfaya, de Nextel, por la información).
  • Aunque tuviéramos acceso a los log de IIS, resultaría complicado saber qué petición corresponde a qué usuario. Es decir, el usuario actual de la petición (según el sistema de seguridad de Membership), se guarda en la cookie .ASPXAUTH y además esta cookie esta encriptada por defecto, así que probablemente sería harto complicado después desencriptar la cookie para saber a qué usuario pertenece la petición.
  • Como queremos que nuestra aplicación sea un Saas puro (soñar es gratis), sólo queremos tener una instancia de la aplicación para dar soporte a distintas empresas y por ende a usuarios de estas empresas. Siendo así, la información de tráfico que pudiéramos obtener desde Windows Azure sería siempre relativa a la instancia y no podríamos discriminar el consumo por cliente.

Una vez habiendo explicado qué motivos nos han conducido a desarrollar nuestra propia solución, es el momento de explicar que información queremos recabar exactamente:

  • Tamaño de la petición.
  • Tamaño de la respuesta.
  • Usuario que generó la petición y respuesta.

Para lograr esto, hemos optado por crear un módulo HTTP.

El primer problema que nos surge es que queremos capturar todo el tráfico con independencia del tipo de recurso solicitado. Esto es que nos da igual si el usuario solicitó un fichero .aspx o un fichero .jpg, un recurso dinámico o un recurso estático, ambos deberían ser procesados por nuestro módulo.

En esta situación es indispensable conocer la versión de IIS donde será desplegada nuestra aplicación. Esto es así porque sólo con IIS 7.0 o superior y con el modelo de canalización integrada, todas las peticiones y con independencia de su tipo, serán gestionadas por ASP.NET y provocarán la ejecución de nuestro módulo.

Aunque en IIS 6.0 podríamos mapear extensiones al filtro ISAPI de ASP.NET, llegamos a la conclusión de que mejor desplegar nuestra aplicación en un Windows Server 2008 y olvidarnos de esta configuración extra.

Cabe mencionar que en cualquier caso, esto es algo que no debería preocupar a nuestro módulo y que sería un hecho transparente al mismo.

Volviendo a la implementación del módulo, éste sería mucho más sencillo si pudiéramos recuperar la cabecera Content-Length desde ASP.NET, pero parecer ser que esta cabecera la crea automáticamente IIS y ASP.NET no conoce de su existencia.

Igualmente, podríamos haber activado en IIS la compresión HTTP, tanto para recursos estáticos como para recursos dinámicos, y de nuevo este hecho es transparente para ASP.NET que no sabe si el recurso solicitado va a ser o no comprimido por IIS.

A este respecto, cabe mencionar que no deberíamos confundir que el cliente acepte la compresión HTTP (según la cabecera Accept-Encoding), a que, efectivamente, se vaya a comprimir la respuesta. Es decir, yo puedo con mi IE aceptar compresión pero el servidor comprimirá o no, eso yo no lo puedo saber.

Otra afirmación que me gustaría compartir, porque no estoy seguro de la misma, es que un cliente nunca comprime la petición. Es decir, un PostBack nunca sube al servidor comprimido con GZIP, son las respuestas del servidor las que pueden o no estar comprimidas, pero la petición nunca.

Como seguro que ya te estás cansando de la previa, vamos directamente a la implementación.

Lo primero es definir una tabla donde guardar la información:

image

  • BandwidthId.
    • Autonúmerico.
  • ApplicationPath.
    • Request.ApplicationPath.
  • Url.
    • Request.RawUrl
  • UserName.
    • User.Identity.Name
  • Request.
    • Request.ContentLength
  • Response.
    • Calculado. La madre del cordero.
  • Compressed.
    • Indica si la respuesta se devolvió comprimida.
  • CompressedResponse.
    • Calculado. La otra madre del cordero.
  • CreatedDate.
    • Fecha de creación automática.
CREATE TABLE [dbo].[Bandwidth](
	[BandwidthId] [int] IDENTITY(1,1) NOT NULL,
	[ApplicationPath] [nvarchar](256) NOT NULL,
	[Url] [nvarchar](max) NOT NULL,
	[UserName] [nvarchar](256) NULL,
	[Request] [int] NOT NULL,
	[Response] [int] NOT NULL,
	[Compressed] [bit] NOT NULL,
	[CompressedResponse] [int] NOT NULL,
	[CreatedDate] [datetime] NOT NULL,
 CONSTRAINT [PK_Bandwidth] PRIMARY KEY CLUSTERED 
(
	[BandwidthId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Bandwidth] ADD  CONSTRAINT [DF_Bandwidth_Compressed]  DEFAULT ((0)) FOR [Compressed]
GO
ALTER TABLE [dbo].[Bandwidth] ADD  CONSTRAINT [DF_Bandwidth_CreatedDate]  DEFAULT (getdate()) FOR [CreatedDate]
GO

El código del módulo es el siguiente, donde a grandes rasgos:



  • Crearemos un módulo HTTP para participar del pipeline de ASP.NET.
  • Crearemos un nuevo filtro en la respuesta con el propósito exclusivo de ir acumulando el total de bytes escritos en el OutputStream.
  • Según configuración, también se guardará una copia del OutputStream en un nuevo Stream del tipo MemoryStream que nos permitirá después jugar con ella sin las limitaciones impuestas por OutputStream.
  • Finalmente, grabaremos un registro con la información obtenida.
  1: Imports System.IO
  2: Imports System.IO.Compression
  3: Imports System.Configuration
  4: Imports System.Web
  5: 
  6: Public Class ContentLengthStream
  7:     Inherits MemoryStream
  8: 
  9:     Private output As Stream
 10:     Private copyOutput As Boolean
 11:     Private totalCount As Integer
 12:     Private memoryStream As MemoryStream
 13: 
 14:     Public Sub New(ByVal output As Stream, ByVal copyOutput As Boolean)
 15:         Me.output = output
 16:         Me.copyOutput = copyOutput
 17:         ' Sólo si copyOutput, se crea el objeto memoryStream
 18:         If copyOutput Then
 19:             memoryStream = New MemoryStream()
 20:         End If
 21:     End Sub
 22: 
 23:     Public Overrides Sub Write(buffer() As Byte, offset As Integer, count As Integer)
 24:         totalCount += count
 25:         HttpContext.Current.Items("TotalCount") = totalCount
 26:         ' Sólo si copyOutput, se escribe en el objeto memoryStream
 27:         If copyOutput Then
 28:             memoryStream.Write(buffer, offset, count)
 29:             HttpContext.Current.Items("OutputStream") = memoryStream
 30:         End If
 31:         output.Write(buffer, offset, count)
 32:     End Sub
 33: End Class
 34: 
 35: Public Class Logger
 36:     Implements IHttpModule
 37: 
 38:     Private Function GetCompressionLength() As Integer
 39:         Dim targetStream As New MemoryStream()
 40:         Dim gzip As New GZipStream(targetStream, CompressionMode.Compress)
 41:         Dim outputStream As MemoryStream = CType(HttpContext.Current.Items("OutputStream"), MemoryStream)
 42:         Dim buffer() As Byte = outputStream.ToArray()
 43:         gzip.Write(buffer, 0, buffer.Length)
 44:         Return targetStream.Length
 45:     End Function
 46: 
 47:     Private Function IsCompressedFileExtension() As Boolean
 48:         Dim extension As String = VirtualPathUtility.GetExtension(HttpContext.Current.Request.RawUrl)
 49:         If extension.StartsWith(".") Then
 50:             extension = extension.Substring(1)
 51:         End If
 52:         Dim q As Integer =
 53:             Aggregate r In ConfigurationManager.AppSettings.Item("BandwidthCompressedFileExtensions").Split(",")
 54:             Where String.Equals(r, extension, StringComparison.OrdinalIgnoreCase)
 55:             Into Count()
 56:         Return If(q = 1, True, False)
 57:     End Function
 58: 
 59:     Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
 60:     End Sub
 61: 
 62:     Public Sub Init(context As System.Web.HttpApplication) Implements System.Web.IHttpModule.Init
 63:         AddHandler context.PostReleaseRequestState, AddressOf PostReleaseRequestState
 64:         AddHandler context.PreSendRequestHeaders, AddressOf PreSendRequestHeaders
 65:     End Sub
 66: 
 67:     Protected Sub PostReleaseRequestState(sender As Object, e As System.EventArgs)
 68:         Dim copyOutput As Boolean
 69:         ' Si el fichero va a ser devuelto comprimido y se quiere calcular el tamaño comprimido
 70:         If IsCompressedFileExtension() AndAlso ConfigurationManager.AppSettings.Item("BandwidthComputeCompression") Then
 71:             copyOutput = True
 72:         End If
 73:         HttpContext.Current.Response.Filter = New ContentLengthStream(HttpContext.Current.Response.Filter, copyOutput)
 74:     End Sub
 75: 
 76:     Protected Sub PreSendRequestHeaders(sender As Object, e As System.EventArgs)
 77:         If HttpContext.Current.Response.StatusCode = 200 Then
 78: 
 79:             Dim requestContentLength As Integer = HttpContext.Current.Request.ContentLength
 80:             Dim responseContentLength As Integer = Convert.ToInt32(HttpContext.Current.Items("TotalCount"))
 81:             Dim compressedResponseContentLength As Integer = 0
 82: 
 83:             If requestContentLength > 0 OrElse responseContentLength > 0 Then
 84: 
 85:                 Dim userName As String = String.Empty
 86:                 If HttpContext.Current.User.Identity.IsAuthenticated Then
 87:                     userName = HttpContext.Current.User.Identity.Name
 88:                 End If
 89: 
 90:                 Dim compressed As Boolean = IsCompressedFileExtension()
 91: 
 92:                 If compressed AndAlso ConfigurationManager.AppSettings.Item("BandwidthComputeCompression") AndAlso responseContentLength > 0 Then
 93:                     compressedResponseContentLength = GetCompressionLength()
 94:                 End If
 95: 
 96:                 Using cnn As New SqlClient.SqlConnection(ConfigurationManager.AppSettings.Item("BandwidthConnectionString"))
 97:                     cnn.Open()
 98:                     Dim cmdText As String = "INSERT INTO Bandwidth (ApplicationPath, Url, UserName, Request, Response, Compressed, CompressedResponse) VALUES (@ApplicationPath, @Url, @UserName, @Request, @Response, @Compressed, @CompressedResponse)"
 99:                     Using cmd As New SqlClient.SqlCommand(cmdText, cnn)
100:                         cmd.Parameters.AddWithValue("@ApplicationPath", HttpContext.Current.Request.ApplicationPath)
101:                         cmd.Parameters.AddWithValue("@Url", HttpContext.Current.Request.RawUrl)
102:                         cmd.Parameters.AddWithValue("@UserName", If(userName = String.Empty, Convert.DBNull, userName))
103:                         cmd.Parameters.AddWithValue("@Request", requestContentLength)
104:                         cmd.Parameters.AddWithValue("@Response", responseContentLength)
105:                         cmd.Parameters.AddWithValue("@Compressed", compressed)
106:                         cmd.Parameters.AddWithValue("@CompressedResponse", compressedResponseContentLength)
107:                         cmd.ExecuteNonQuery()
108:                     End Using
109:                 End Using
110:             End If
111:         End If
112: 
113:         If Not HttpContext.Current.Items("OutputStream") Is Nothing Then
114:             CType(HttpContext.Current.Items("OutputStream"), MemoryStream).Dispose()
115:             HttpContext.Current.Items("OutputStream") = Nothing
116:         End If
117:     End Sub
118: End Class

Los parámetros de configuración que utiliza nuestro módulo a través de appSettings del fichero web.config son los siguientes:



  • BanwidthConnectionString.

    • Cadena de conexión contra un servidor Sql Server donde se guardará la información obtenida.

  • BandwidthCompressedFileExtensions.

    • Lista de extensiones separadas por comas, que sabemos van a ser devueltas comprimidas por IIS. Reitero que esta información es necesaria porque desde el módulo de ASP.NET no somos capaces de saber si el recurso va a ser o no devuelto comprimido. Siendo así, hay que especificarlo manualmente.

  • BandwidthComputeCompression.

    • Indica si cuando encontramos un recurso que va ser devuelto comprimido (según BandwidthCompressedFileExtensions), nuestro módulo debe comprimir la copia de OutputStream para grabar el dato de la columna CompressedResponse.

Esta claro que el parámetro más agresivo es BandwidthComputeCompression, que en el caso de estar activo, fuerza al módulo a crear una copia de Response.OutputStream en memoria para después comprimirla con GZip. Exactamente no sabría calibrar el impacto de esta tarea por cada petición, pero está claro que entre hacerlo o no hacerlo, es más óptimo no hacerla.


En cualquier caso, siempre se podría desactivar este parámetro y luego hacer un ratio aproximado de cuanto sería comprimida la respuesta a través de una consulta Sql.


Lo que quiero decir con aproximado es que tengo claro que esta solución no es un capturador de tráfico definitivo por lo siguientes motivos:



  • Sólo captura tráfico del cuerpo de la petición. Esto es que las cabeceras de la respuesta no están incluidas.
  • Si comprimimos la respuesta, he visto con la ayuda de Fiddler, que mi compresión no es el mismo valor que la compresión que realmente devuelve Fidderl, de hecho ni el propio Fiddler arroja el mismo valor si cambiamos entre “No Compression” y “GZip Encoding”. Por ejemplo:

Respuesta original (362 bytes)


image


Ahora pulso en “No Compression” y obtengo 337 bytes


image


Ahora pulso de nuevo en GZIP Encoding y obtengo… 250 bytes! (no 362 como era de esperar)


image


Esta claro que en 2 días no nos vamos a hacer un super-módulo sniffer de tráfico HTTP, pero al menos queremos tener una información muy aproximada de que consumo de tráfico genera que usuario.


En cualquier caso, este módulo está aún en fase beta y el tiempo nos dará o nos quitará la razón.


Por último, un fichero web.config válido para hacer ejecutar el módulo sería el siguiente:

<?xml version="1.0"?>
<configuration>
  <appSettings>
    <add key="BandwidthConnectionString" value="Data Source=(local);Initial Catalog=Bandwidth;Persist Security Info=True;User ID=sa;Password=******"/>
    <add key="BandwidthComputeCompression" value="True"/>
    <add key="BandwidthCompressedFileExtensions" value="aspx,js,htm"/>
  </appSettings>
  <system.web>
    <compilation strict="false" explicit="true" targetFramework="4.0"/>
  </system.web>
  <system.webServer>
    <modules>
      <add name="Bandwidth" type="Bandwidth.Logger"/>
    </modules>
  </system.webServer>
</configuration>

Un saludo!