View State

by Nicolás Ferreira 31. July 2007 00:00

La mayoría de las veces tenemos que almacenar información entre peticiones de página. Como siempre, tenemos muchas maneras de hacer esto. La regla general es que si lo que precisamos es seguridad, administramos el estado del lado del servidor. Si lo que precisamos es rendimiento utilizamos administración de estado del lado del cliente.
Al utilizar administración de estado del lado del cliente es recomendable no almacenar información sensible. El método más común para almacenar información del lado del cliente es utilizar View State. ASP.NET utiliza View State para registrar los valores en los controles.
Generalmente las personas (incluso yo lo creía) creen que el View State está encriptado. Esto no es así. El View State es información serializada que después para que pueda ser almacenada en el HTML se convierte a una cadena Base-64.

Demostración:

        protected void Page_Load(object sender, EventArgs e)
        {
            ViewState.Add("NicolasFerreira", "http://www.nicolasferreira.com/");
        }

Salida HTML:

    <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUJNzgzNDMwNTMzDxYCHg9OaWNvbGFzRmVycmVpcmEFH2h0dHA6Ly93d3cubmljb2xhc2ZlcnJlaXJhLmNvbS9kZJUtdPnrrPUmYC4xJVygWF1HWkH4" />

Si ponemos el siguiente código:

        protected void Page_Load(object sender, EventArgs e)
        {
            LosFormatter lLosFormatter = new LosFormatter();
            Pair lPair = (Pair)lLosFormatter.Deserialize
                ("/wEPDwUJNzgzNDMwNTMzDxYCHg9OaWNvbGFzRmVycmVpcmEFH2h0dHA6Ly93d3cubmljb2xhc2ZlcnJlaXJhLmNvbS9kZJUtdPnrrPUmYC4xJVygWF1HWkH4");
            ArrayList lArrayList = (ArrayList)((Pair)((Pair)lPair.First).Second).First;
            System.Web.UI.IndexedString lIndexedString = (System.Web.UI.IndexedString)lArrayList[0];
            string lstring = (string)lArrayList[1];
        }

Y analizamos el valor de la propiedad "lIndexedString.Value" y el valor de la variable "lstring" vamos a ver lo siguiente:

lIndexedString.Value = "NicolasFerreira"
lstring = http://www.nicolasferreira.com/

Lo que demuestra esto es que por defecto el View State no viene encriptado.
Aunque el código no se encuentre claro esto es simplemente para demostrar que el View State no viene encriptado por defecto.
Vamos a lo que nos interesa... encriptar el View State estableciendo el ajuste en el archivo Web.config (también lo podemos hacer en la directiva @Page para cada página):

<configuration>
  <system.web>
    <pages viewStateEncryptionMode="Always"/>
  </system.web>
</configuration>

Si repetimos el primer ejemplo:

        protected void Page_Load(object sender, EventArgs e)
        {
            ViewState.Add("NicolasFerreira", "http://www.nicolasferreira.com/");
        }

Vamos a obtener algo como esto:

<input type="hidden" name="__VIEWSTATE" id="Hidden1" value="NjodhOfUMQ+Kgz07a092dExYHtdZWVkyuNupSqW0xvO/4lwpBtc3LbYKkBKD21GbNaxzWvLvuGfO9zSzRg7HjmL3JZlHeQVjvvHNAxKAfp0=" />

Es obvio que "value" no se ve como una cadena codificada en Base-64, por consiguiente, View State está encriptado.
Si intentamos repetir el segundo ejemplo pasando esta cadena al método Deserialize de LosFormatter vamos a obtener una excepción:

System.ArgumentException
{"Los datos serializados no son válidos."}

Tags:

.NET Development

Concatenando strings, Declarando variables.

by Nicolás Ferreira 29. July 2007 00:00

1 - Concatenando strings

C#:

        private string Ejemplo1()
        {
            return "Hola " + "Mundo";
        }

MSIL:

.method private hidebysig instance string
        Ejemplo1() cil managed
{
  .maxstack  1
  .locals init ([0] string CS$1$0000)
  IL_0000:  nop
  IL_0001:  ldstr      "Hola Mundo"
  IL_0006:  stloc.0
  IL_0007:  br.s       IL_0009
  IL_0009:  ldloc.0
  IL_000a:  ret
}

Como pueden ver en el código MSIL, el compilador unió las dos cadenas. Aquí no estamos consumiendo recursos innecesariamente.

C#:

        private string Ejemplo2()
        {
            string lEjemplo1 = "Hola ";
            lEjemplo1 += "Mundo";
            return lEjemplo1;
        }

MSIL:

.method private hidebysig instance string
        Ejemplo2() cil managed
{
  .maxstack  2
  .locals init ([0] string lEjemplo1,
           [1] string CS$1$0000)
  IL_0000:  nop
  IL_0001:  ldstr      "Hola "
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  ldstr      "Mundo"
  IL_000d:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_0012:  stloc.0
  IL_0013:  ldloc.0
  IL_0014:  stloc.1
  IL_0015:  br.s       IL_0017
  IL_0017:  ldloc.1
  IL_0018:  ret
}

Este caso se ve a menudo cuando lo que se intenta hacer son saltos de línea. Aquí al haber utilizado "+=" estamos llamando a String.Concat() y creando una nueva cadena temporal. Mi recomendación es no utilizar nunca "+=" para concatenar cadenas. Visual Studio Team System tiene una advertencia de rendimiento llamada "DoNotConcatenateStringsInsideLoops" (CA1818) que habla de no utilizar el método System.String.Concat o los operadores de adición (+ ó &) o de asignación de adición (+=) dentro de los bucles.

Un método al parecer más eficiente de concatenar cadenas es utilizar String.Concat(String[]). Internamente lo que hace es contabilizar la longitud necesaria para la cadena resultante y crear una variable cadena temporal del largo correcto. Lo mismo se aplica al método String.Concat(Object[]) con la diferencia que concatena la representación cadena de los elementos que se especifican en la matriz Object.

El siguiente ejemplo muestra cómo el compilador hace uso de String.Concat(Object[]):

C#:

        private string Ejemplo3(string pNombre, int pTiempoTranscurrido)
        {
            return "Bienvenido " + pNombre + ", quedan " +
                pTiempoTranscurrido + " día(s) para que el período de evaluación finalice.";
        }

MSIL:

.method private hidebysig instance string
        Ejemplo3(string pNombre,
                 int32 pTiempoTranscurrido) cil managed
{
  .maxstack  3
  .locals init ([0] string CS$1$0000,
           [1] object[] CS$0$0001)
  IL_0000:  nop
  IL_0001:  ldc.i4.5
  IL_0002:  newarr     [mscorlib]System.Object
  IL_0007:  stloc.1
  IL_0008:  ldloc.1
  IL_0009:  ldc.i4.0
  IL_000a:  ldstr      "Bienvenido "
  IL_000f:  stelem.ref
  IL_0010:  ldloc.1
  IL_0011:  ldc.i4.1
  IL_0012:  ldarg.1
  IL_0013:  stelem.ref
  IL_0014:  ldloc.1
  IL_0015:  ldc.i4.2
  IL_0016:  ldstr      ", quedan "
  IL_001b:  stelem.ref
  IL_001c:  ldloc.1
  IL_001d:  ldc.i4.3
  IL_001e:  ldarg.2
  IL_001f:  box        [mscorlib]System.Int32
  IL_0024:  stelem.ref
  IL_0025:  ldloc.1
  IL_0026:  ldc.i4.4
  IL_0027:  ldstr      bytearray (20 00 64 00 ED 00 61 00 28 00 73 00 29 00 20 00   //  .d...a.(.s.). .
                                  70 00 61 00 72 00 61 00 20 00 71 00 75 00 65 00   // p.a.r.a. .q.u.e.
                                  20 00 65 00 6C 00 20 00 70 00 65 00 72 00 ED 00   //  .e.l. .p.e.r...
                                  6F 00 64 00 6F 00 20 00 64 00 65 00 20 00 65 00   // o.d.o. .d.e. .e.
                                  76 00 61 00 6C 00 75 00 61 00 63 00 69 00 F3 00   // v.a.l.u.a.c.i...
                                  6E 00 20 00 66 00 69 00 6E 00 61 00 6C 00 69 00   // n. .f.i.n.a.l.i.
                                  63 00 65 00 2E 00 )                               // c.e...
  IL_002c:  stelem.ref
  IL_002d:  ldloc.1
  IL_002e:  call       string [mscorlib]System.String::Concat(object[])
  IL_0033:  stloc.0
  IL_0034:  br.s       IL_0036
  IL_0036:  ldloc.0
  IL_0037:  ret
}

Para ir terminando voy a citar al MSDN:

"Consideraciones de rendimiento

Los métodos Concat y AppendFormat concatenan los nuevos datos en un objeto String o StringBuilder existente. La operación de concatenación en un objeto String siempre crea un nuevo objeto a partir de la cadena existente y los nuevos datos. Un objeto StringBuilder mantiene un búfer para alojar la concatenación de nuevos datos. Los nuevos datos se anexan al final del búfer si hay espacio disponible; de lo contrario, se asigna un nuevo búfer más grande, los datos del búfer original se copian en el nuevo búfer y, a continuación, los nuevos datos se anexan al nuevo búfer.

El rendimiento de una operación de concatenación para un objeto String o StringBuilder depende de la frecuencia con que se produzca la asignación de memoria. La operación de concatenación en un objeto String siempre asigna memoria, mientras que la operación de concatenación en un objeto StringBuilder sólo asigna memoria si el búfer del objeto StringBuilder es demasiado pequeño para alojar los nuevos datos. Por ello, la clase String es preferible para una operación de concatenación si uno concatena un número fijo de objetos String. En ese caso, el compilador podría combinar en una única operación cada una de las operaciones de concatenación. Los objetos StringBuilder son preferibles para las operaciones de concatenación si se concatena un número arbitrario de cadenas; por ejemplo, si un bucle concatena un número aleatorio de cadenas de datos proporcionados por el usuario."

2 - Declarando variables

C#:

        private void Ejemplo4()
        {
            if (Environment.OSVersion.Platform != PlatformID.Win32NT)
            {
                Application.Exit();
            }
            else
            {
                StringBuilder lStringBuilder = new StringBuilder();
            }
        }

MSIL:

.method private hidebysig instance void  Ejemplo4() cil managed
{
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Text.StringBuilder lStringBuilder,
           [1] bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  call       class [mscorlib]System.OperatingSystem [mscorlib]System.Environment::get_OSVersion()
  IL_0006:  callvirt   instance valuetype [mscorlib]System.PlatformID [mscorlib]System.OperatingSystem::get_Platform()
  IL_000b:  ldc.i4.2
  IL_000c:  ceq
  IL_000e:  stloc.1
  IL_000f:  ldloc.1
  IL_0010:  brtrue.s   IL_001c
  IL_0012:  nop
  IL_0013:  call       void [System.Windows.Forms]System.Windows.Forms.Application::Exit()
  IL_0018:  nop
  IL_0019:  nop
  IL_001a:  br.s       IL_0024
  IL_001c:  nop
  IL_001d:  newobj     instance void [mscorlib]System.Text.StringBuilder::.ctor()
  IL_0022:  stloc.0
  IL_0023:  nop
  IL_0024:  ret
}

La palabra clave "init" le dice al compilador JIT que debe inicializar todas las variables locales antes que comience la ejecución del método. Para todas las variables tipo de valor se llama al constructor por defecto correspondiente, y todas las variables tipo de referencia se establecen a nulo. El resultado será igual si hacemos esto:

C#:

        private void Ejemplo5()
        {
            StringBuilder lStringBuilder;
            if (Environment.OSVersion.Platform != PlatformID.Win32NT)
            {
                Application.Exit();
            }
            else
            {
                lStringBuilder = new StringBuilder();
            }
        }

No vamos a obtener mejor rendimiento utilizando el código del ejemplo 4 o el código del ejemplo 5. El resultado será el mismo.

Tags:

.NET Development

Cross-page Posting

by Nicolás Ferreira 27. July 2007 00:00

A menudo tenemos que pasar información entre formularios. Esto lo podemos hacer de diferentes maneras. La buena (para mi) implica utilizar algo denominado "Cross-page Posting" (también depende de la situación, pero en general funciona). La mala (también para mí) implica utilizar técnicas como por ejemplo guardar la información en sesión. Cross-page Posting se da cuando un control está configurado para realizar PostBack a una página Web diferente. Para aclarar todo esto vamos a empezar con un ejemplo en donde tenemos la página "padre" y la página "hija".

En el code-behind de la página padre exponemos propiedades públicas para que puedan ser accedidas por la página hija.

        public string Contraseña
        {
            get
            {
                return this.TxtContraseña.Text;
            }
        }

        public string NombreUsuario
        {
            get
            {
                return this.TxtNombreUsuario.Text;
            }
        }

Establecemos la propiedad PostBackUrl del botón que va a ser el encargado de hacer el PostBack a la página hija con la ruta virtual de la página hija, quedando el código de marcas de la siguiente manera:

        <asp:Label ID="LblNombreUsuario" runat="server" Text="Nombre De Usuario:"></asp:Label>
        <asp:TextBox ID="TxtNombreUsuario" runat="server"></asp:TextBox>
        <asp:Label ID="LblContraseña" runat="server" Text="Contraseña:"></asp:Label>
        <asp:TextBox ID="TxtContraseña" runat="server"></asp:TextBox>
        <asp:Button ID="BtnAceptar" runat="server" PostBackUrl="~/Child.aspx" Text="Aceptar" />

Ahora en el código de marcas de la página hija debajo de la directiva de página ponemos lo siguiente:

 

        <%@ PreviousPageType VirtualPath="~/Parent.aspx" %>

 

Compilamos la aplicación para que IntelliSense agarre desde el code-behind de la página hija lo siguiente:

    protected void Page_Load(object sender, EventArgs e)
    {
        if (PreviousPage != null)
        {
            this.LblContraseñaValor.Text =
                Server.HtmlEncode(this.PreviousPage.Contraseña);
            this.LblNombreUsuarioValor.Text =
                Server.HtmlEncode(this.PreviousPage.NombreUsuario);
        }
    }

Al haber agregado en el código de marcas de la página hija la directiva PreviousPageType y establecido VirtualPath a "~/Parent.aspx", desde el code-behind de la página hija la propiedad PreviousPage toma el tipo de la página padre. El resultado es que tenemos un Cross-page Posting de tipo seguro.

Tags:

.NET Development

FileUpload

by Nicolás Ferreira 27. July 2007 00:00

A continuación voy a demostrar cómo crear un sitio Web sencillo para subir archivos al servidor.

1º: Abrimos Visual Studio 2005.
2º: Menú File -> New -> Web Site...
3º: Seleccionamos ubicación (en este caso voy a elegir File System), y después Language (para este ejemplo voy a utilizar Visual Basic).
4º: Clic en OK.

Vamos a ver que Visual Studio crea automáticamente una página llamada Default.aspx y nos deja en el código de marcas.

5º: Hacemos clic derecho sobre Default.aspx en el explorador de solución y elegimos View Desginer.
6º: Desde el Toolbox, arrastramos hacia la página un control FileUpload y otro control Button.
7º: Hacemos doble clic en el control Button para que Visual Studio cree el manejador de evento por defecto (para el control Button es Click):

    Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click

    End Sub

Lo primero que tenemos que hacer es verificar que el usuario haya puesto algún archivo en el control FileUpload (que en HTML se renderiza como input type="submit").

    Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
        If (Me.FileUpload1.HasFile) Then
            'Ha puesto un archivo.
        End If
    End Sub

Opcionalmente, podemos hacer validaciones como por ejemplo saber el tipo MIME del archivo, el tamaño en bytes del archivo subido, el nombre del archivo.

    Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
        If (Me.FileUpload1.HasFile And Me.FileUpload1.PostedFile.ContentLength > 0 And Me.FileUpload1.PostedFile.ContentType = "image/bmp") Then
        End If
    End Sub

Después de esto podemos tomar diferentes caminos dependiendo de lo que se desee hacer. Para guardar el archivo a una carpeta podemos hacer lo siguiente:

    Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
        If (Me.FileUpload1.HasFile And Me.FileUpload1.PostedFile.ContentLength > 0 And Me.FileUpload1.PostedFile.ContentType = "image/bmp") Then
            Me.FileUpload1.SaveAs(Server.MapPath(System.IO.Path.Combine("~/Uploads/", System.IO.Path.GetFileName(FileUpload1.PostedFile.FileName))))
        End If
    End Sub

Con Server.MapPath lo que hacemos es obtener la ruta física en el servidor Web que corresponde a la ruta virtual especificada.
Con System.IO.Path.Combine combinamos dos rutas.
Con System.IO.Path.GetFileName obtenemos el nombre de archivo con la extensión de la ruta especificada. Si vemos el código, le paso FileUpload1.PostedFile.FileName. FileUpload1.PostedFile.FileName es el nombre del archivo que el usuario tiene en su computadora.
Por supuesto que a esto le faltan bastantes validaciones, como por ejemplo en el caso de que el directorio Uploads no exista la aplicación se va a caer. Podemos arreglarlo haciendo esto:

    Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
        If (Me.FileUpload1.HasFile And Me.FileUpload1.PostedFile.ContentLength > 0 And Me.FileUpload1.PostedFile.ContentType = "image/bmp") Then
            If Not (System.IO.Directory.Exists(Server.MapPath("~/Uploads/"))) Then
                System.IO.Directory.CreateDirectory(Server.MapPath("~/Uploads/"))
            End If
            Me.FileUpload1.SaveAs(Server.MapPath(System.IO.Path.Combine("~/Uploads/", System.IO.Path.GetFileName(FileUpload1.PostedFile.FileName))))
        End If
    End Sub

También lo que va a pasar si alguien intenta subir un archivo con un nombre de archivo que ya existe en la carpeta Uploads es que el archivo viejo se va a sobrescribir.
Una cosa que hay que tener en cuenta es el límite de tamaño que el cliente tiene al subir un archivo. Para averiguar la configuración del sitio para saber a cuánto estamos limitados podemos utilizar lo siguiente:

    Private Function MaxRequestLength() As Integer
        Dim lMaxRequestLengthRetorno As Integer
        Dim lObjHttpRuntimeSection As Object = System.Web.Configuration.WebConfigurationManager.GetWebApplicationSection("system.web/httpRuntime")
        If (Not lObjHttpRuntimeSection Is Nothing And lObjHttpRuntimeSection.GetType Is GetType(System.Web.Configuration.HttpRuntimeSection)) Then
            Dim lHttpRuntimeSection As System.Web.Configuration.HttpRuntimeSection = DirectCast(lObjHttpRuntimeSection, System.Web.Configuration.HttpRuntimeSection)
            lMaxRequestLengthRetorno = lHttpRuntimeSection.MaxRequestLength
        End If
        Return lMaxRequestLengthRetorno
    End Function

MSDN dice lo siguiente sobre MaxRequestLength:

Valor de propiedad
Tamaño máximo de la solicitud en kilobytes. El tamaño predeterminado son 4096 KB (4 MB).

Comentarios
Se puede utilizar la propiedad MaxRequestLength para evitar ataques de denegación de servicio producidos por usuarios que envían archivos de gran tamaño al servidor.

Para cambiar el tamaño, agregamos un archivo de configuración Web y dentro de la sección system.web especificamos lo siguiente:

  <system.web>
    <httpRuntime maxRequestLength="4096"/>
  </system.web>

Tags:

.NET Development