Blog dedicado a la programación .NET y la informática en general

Archivo para la Categoría "Programación .NET"

Logging en fichero de texto con Enterprise Library 6 y C#

Se plantea la posibilidad de guardar los sucesos de una aplicación en un fichero de texto.

Para ello se va a hacer uso de las librerías de Enterprise Library en su versión 6 mediante su módulo “Logging Application Block” y el ejemplo se ha realizado sobre Visual Studio 2012 en un proyecto WPF.

El primer paso es instalar las librerías mediante “NuGet” haciendo click con el botón derecho sobre el proyecto y seleccionando “Administrar paquetes NuGet”

Buscar por “EnterpriseLibrary” e instalar “Enterprise Library – Logging Application Block”.

Se instalará las librerías correspondientes a:

EnterpriseLibrary.Common

EnterpriseLibrary.Logging

De forma alternativa se puede instalar el paquete mediante la línea de comandos de NuGet ejecutando el siguiente comando:

PM> Install-Package EnterpriseLibrary.Logging

Ahora hay que configurar el fichero .config para que nos permita guardar los sucesos en un fichero de texto. Para esto tenemos tres posibilidades de edición. La primera y la segundas son las más laboriosas, la primera es editando directamente el fichero .config y la segunda realizarlo en tiempo de ejecución por código fuente, ambas según las directivas indicadas en la documentación:

http://msdn.microsoft.com/en-us/library/dn169621.aspx

La tercera forma, es la que os recomiendo si os acostumbráis a usar “Enterprise Library”, es descargando el software de gestión de ficheros de configuración que os encontraréis en la página web.

Nota: para la versión 5 es más fácil aún, ya que se puede instalar por NuGet el paquete “EnterpriseLibrary.Config” para VisualStudio y permite la edición del fichero .config desde el mismo proyecto haciendo click con el botón derecho del ratón sobre el proyecto que tenga instalada las librerías de Enterprise Library.

http://www.microsoft.com/en-us/download/details.aspx?id=38789

Basta con descargarse el fichero “EnterpriseLibrary6-binaries.exe”.

Dentro de este se encuentra el fichero con el nombre “Microsoft Enterprise Library 6.zip” que comprime los ensamblados que necesitaremos.

Ejecutar la aplicación “EntLibConfig.exe” (dando por supuesto que usáis un sistema operativo de 64 bits).

Abrimos el fichero de configuración del proyecto:

FileàOpen y buscar el fichero .config del proyecto en el que se ha instalado las librerías.

Añadir el bloque de configuración para el registrar los sucesos.

Por defecto introduce los registros en el visor de sucesos del sistema operativo. Se añade el soporte para el registro sobre un fichero de texto.

Hay dos opciones, registrarlo sobre un único fichero de texto plano o sobre varios. El segundo nos permite controlar cambiar el fichero de texto según la fecha o el tamaño que ocupe. En este ejemplo se trabajará sobre un único fichero de texto plano por ser más sencillo.

Ahora se debe asociar una configuración que de formato a los sucesos en el fichero. Para ello se reutilizará el que está definido para el visor de sucesos.

La configuración se finaliza cambiando el “listener” por defecto de las categorías “General” y “Logging Errors & Warnings”.

Se salvan los cambios y volvemos a Visual Studio para comprobar los cambios en el fichero de configuración.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <section name="loggingConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.LoggingSettings, Microsoft.Practices.EnterpriseLibrary.Logging, Version=6.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="true" />
    </configSections>
    <loggingConfiguration name="" tracingEnabled="true" defaultCategory="General">
        <listeners>
            <add name="Event Log Listener" type="Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners.FormattedEventLogTraceListener, Microsoft.Practices.EnterpriseLibrary.Logging, Version=6.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
                listenerDataType="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.FormattedEventLogTraceListenerData, Microsoft.Practices.EnterpriseLibrary.Logging, Version=6.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
                source="Enterprise Library Logging" formatter="Text Formatter"
                log="" machineName="." traceOutputOptions="None" />
            <add name="Flat File Trace Listener" type="Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners.FlatFileTraceListener, Microsoft.Practices.EnterpriseLibrary.Logging, Version=6.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
                listenerDataType="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.FlatFileTraceListenerData, Microsoft.Practices.EnterpriseLibrary.Logging, Version=6.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
                fileName="trace.log" formatter="Text Formatter" />
        </listeners>
        <formatters>
            <add type="Microsoft.Practices.EnterpriseLibrary.Logging.Formatters.TextFormatter, Microsoft.Practices.EnterpriseLibrary.Logging, Version=6.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
                template="Timestamp: {timestamp}{newline}
Message: {message}{newline}
Category: {category}{newline}
Priority: {priority}{newline}
EventId: {eventid}{newline}
Severity: {severity}{newline}
Title:{title}{newline}
Machine: {localMachine}{newline}
App Domain: {localAppDomain}{newline}
ProcessId: {localProcessId}{newline}
Process Name: {localProcessName}{newline}
Thread Name: {threadName}{newline}
Win32 ThreadId:{win32ThreadId}{newline}
Extended Properties: {dictionary({key} - {value}{newline})}"
                name="Text Formatter" />
        </formatters>
        <categorySources>
            <add switchValue="All" name="General">
                <listeners>
                    <add name="Flat File Trace Listener" />
                </listeners>
            </add>
        </categorySources>
        <specialSources>
            <allEvents switchValue="All" name="All Events" />
            <notProcessed switchValue="All" name="Unprocessed Category" />
            <errors switchValue="All" name="Logging Errors &amp; Warnings">
                <listeners>
                    <add name="Flat File Trace Listener" />
                </listeners>
            </errors>
        </specialSources>
    </loggingConfiguration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
</configuration>

Para usarlo en el código usaremos la clase estática “Logger”, pero previamente hay que asignarle el “TraceListener” por defecto. Por tanto es conveniente añadir un método que lo esteblezca en el inico del aplicativo, por ejemplo el constructor del archivo App.xaml para WPF o en Global.asax para ASP.NET o Program.cs en WindowsForms.

/// <summary>
/// Inicializa LAB en la aplicación para que se puedan registrar sucesos
/// </summary>
private void InicarLibreriaLog()
{
 IConfigurationSource configurationSource = ConfigurationSourceFactory.Create();
 LogWriterFactory logWriterFactory = new LogWriterFactory(configurationSource);
 Logger.SetLogWriter(logWriterFactory.Create());
}

Una vez que se tiene asignado el “Listener” se puede proceder a registrar los sucesos como se muestra acontinuación.

private void btnRegistrar_Click(object sender, RoutedEventArgs e)
{
 string suceso = this.txtSuceso.Text;
 Logger.Write(suceso);
 MessageBox.Show(&amp;quot;Suceso guardado&amp;quot;);
}

El suceso será almacenado en un fichero de texto que para la configuración que hemos establecido se llamará “trace.log” y estará ubicado en la carpeta “bin” del proyecto.

Anuncios

Thread, BackgroundWorker y Task… implementación de métodos asíncronos

En este artículo me voy a centrar en la implementación de los diferentes métodos que existen en .NET para invocar funciones de forma asíncrona, pudiendo informar del progreso del mismo y también ofreciendo una forma de poder cancelar el mismo proceso.

Al final del artículo adjuntaré la solución para Visual Studio con el código del artículo que podréis descargar desde OneDrive.

Cuando comienzo la programación de un método tengo la mala costumbre de no pensar si dicho método puede ser más útil si permite su ejecución de forma asíncrona y esto en ocasiones me cuesta que el proceso global sea más pesado o que incluso la aplicación se quede en modo de espera más tiempo del que debiera no pudiendo ni informar ni cancelar la ejecución. Por esta razón me he decidido a realizar este artículo, para comprender mejor el funcionamiento de los procesos asíncronos y también para iniciarme en la ejecución de métodos de forma paralela, ya que hoy en día con tantos núcleos por procesador podemos reducir considerablemente el tiempo de ejecución de un método síncrono.

No me voy a meter en contar la historia de los procesos asíncronos, para ello os invito a que busquéis en internet, yo iré más al grano, a la implementación. Probablemente si estáis leyendo este artículo será porque desconocéis algunas de las formas que existen para realizar métodos asíncronos. Es relativamente fácil llamar a un método asíncrono que no te devuelva nada o del que no necesitas información, pero en el momento de tener que pasarle parámetros de control la cosa cambia.

Al grano

Actualmente y por razones históricas existen 3 formas de realizar llamadas asíncronas que nos permiten ejecutar el código en hilos de ejecución distintos y por tanto poder ser procesadas de forma paralela.

Los métodos de implementación los dividiré por sus clases en .NET, más concretamente en la versión de Framework 4.5.

El primero históricamente es la clase Thread disponible desde la versión 1.0 de Framework. De su encapsulación y mejora apareció para el Framewok 2.0 la clase BackgroundWorker donde se intenta mejorar el desarrollo de la ejecución de operaciones que lleven un tiempo considerable encapsulando en esta su invocación, información del progreso y posibilidad de cancelación del proceso asíncrono. Y por último, y ante la avalancha de núcleos por procesador y el potencial que se nos brindaba con la programación paralela, surgió la clase Task desde la versión 4.0 de Framework, aunque en la versión 4.5 se ha potenciado considerablemente, y por tanto será en la que me centre.

La clase Thread

Para invocar a un método de forma asíncrona deberemos tener en cuenta si a este se le ha de pasar parámetros y si de ha de devolver resultados. Me centraré en lo más práctico y amplio del concepto, la llamada asíncrona pasándole parámetros y esperando un resultado.

Thread – Invocación asíncrona

// Lanzamiento del hilo de ejecución asíncrono
Thread hilo = new Thread(tp.ConvertirNumero);
hilo.Start(5);

El método “ConvertirNumero” nos devolverá un número entero en forma de texto y a la vez ralentizará la conversión para poder comprobar los resultados.

El método síncrono para poder ser llamado de forma síncrona por “Thread” debe cumplir ciertos requisitos. Por ejemplo devolver “void” y que los parámetros estén encapsulados en un único objeto global “Object”. Os preguntaréis, si tiene que devolver “void” ¿cómo voy a obtener el resultado de la ejecución?, lo vemos en el siguiente apartado, tranquilos.

Thread – Control de progreso y resultado

La única forma de saber qué está haciendo el proceso asíncrono es mediante delegados y eventos y devolviendo una clase con los valores que creamos necesarios, como por ejemplo el progreso de la operación o el resultado del mismo.

Por tanto en el ejemplo para evitar tener que declarar un delegado personalizado he usado “EventHandler” y me he creado una clase personalizada que hereda de “EventArgs” con el progreso y el resultado del proceso que rescataré más adelante.

/// <summary>
/// Evento para controlar el progreso y resultado del proceso asíncrono con Thread
/// </summary>
public event EventHandler Procesando;

//...

public class ProgresoEventArgs : EventArgs
{
public string MensajeProgreso { get; set; }
public string Resultado { get; set; }
}

Durante la ejecución o al final de ella podremos informar del progreso comprobando previamente si el evento ha sido asociado a un gestor de eventos por código, tal y como muestro acontinuación.

// Una vez finalizado usa el mismo evento de información de progreso para indicarlo
if (Procesando != null)
{
ProgresoEventArgs ea = new ProgresoEventArgs();
ea.MensajeProgreso = ObtenerProgreso(x, cuenta);
ea.Resultado = x.ToString();
Procesando(this, ea);
}

Thread – Cancelación de proceso asíncrono

Para poder cancelar el proceso necesitamos hacer uso de una variable que sea accesible desde cualquier hilo de ejecución para poder ser modificada y vigilada. Para ello anteriormente se usaba una variable que debía ser manejada con la instrucción “lock”, que esperaba a que la variable fuese liberada en otro hilo de ejecución antes de ser comprobada o modificada en el actual. Ahora no hace falta esa implementación gracias al tipo “volatile”.

/// <summary>
/// Para la ejecución asíncrona con Threads se necesita una variable que indique si es necesaria la detención del método
/// y que sea invocable de forma pública
/// </summary>
private volatile bool detener;

Dicha variable será vigilada durante el proceso asíncrono para detenerlo cuando cambie su estado. En mi ejemplo se cambia gracias a un método público de la clase.

while (cuenta < x)
{
//...

// Comprueba si ha de detenerse el proceso
if (detener)
{
break;
}
}

Thread – Proceso asíncrono

El código del proceso asíncrono, ya uniendo el control de cancelación, progreso y resultado quedaría de la siguiente forma:

/// <summary>
/// Método para usarlo con Threads
/// </summary>
/// <param name="xObj">Necesita que el parámetro de entrada sea una clase genérica Object para poder pasarle cualquier clase con los parámetros necesarios</param>
public void ConvertirNumero(object xObj)
{
// Inicialiaza la variable que indica si ha de detenerse el proceso asíncrono
detener = false;

int x = (int)xObj;
int cuenta = 0;
while (cuenta < x)
{
Thread.Sleep(500);
cuenta++;

// Comprueba si el evento que indica el proceso está siendo manejado o no
if (Procesando != null)
{
// Indica el progreso
ProgresoEventArgs ea = new ProgresoEventArgs();
ea.MensajeProgreso = ObtenerProgreso(x, cuenta);
Procesando(this, ea);
}

// Comprueba si ha de detenerse el proceso
if (detener)
{
break;
}
}

// Una vez finalizado usa el mismo evento de información de progreso para indicarlo
if (Procesando != null)
{
ProgresoEventArgs ea = new ProgresoEventArgs();
ea.MensajeProgreso = ObtenerProgreso(x, cuenta);
ea.Resultado = x.ToString();
Procesando(this, ea);
}
}

La clase BackgroundWorker

Esta clase ya viene preparada para poder pasarle parámetros, recoger el resultado de la operación y la gestión de eventos de progreso, cancelación y finalización.

BackgroundWorker – Construcción del objeto

Para los que seguimos usando los formularios Windows y nos gusta usar las barras de progreso esta clase está disponible como un control en la barra de herramientas que puedes arrastrar hasta tu formulario.

Captura

Lo primero que hay que hacer cuando se construye el objeto es indicarle si vamos a manejar el progreso y la cancelación del método asíncrono y por tanto asociarle los eventos de control.

// Inicialización del BackgroundWorker
bw.WorkerReportsProgress = true;
bw.WorkerSupportsCancellation = true;
bw.DoWork += bw_DoWork;
bw.ProgressChanged += bw_ProgressChanged;
bw.RunWorkerCompleted += bw_RunWorkerCompleted;

Como podréis comprobar a diferencia de “Thread” la clase ya tiene implementada la metodología necesaria.

BackgrounWorker – Gestores de Eventos

La invocación al método asíncrono sería de la siguiente forma:

private void btnAsincrono_Click(object sender, EventArgs e)
{
this.btnCancelar.Enabled = true;
this.btnAsincrono.Enabled = false;
// Este método se encarga de invocar al evento asíncrono DoWork
bw.RunWorkerAsync(new object[] { 5 });
}

Y dentro del gestor del evento “DoWork” es cuando se llama al método síncrono que queremos controlar. Para ello también debe cumplir ciertos requisitos, como pasarle el objeto “BackgroundWorker” que lo está controlando para poder informar del progreso, y también el objeto de la clase “CancelEventArgs” para vigilar si el proceso ha de finalizarse por petición del usuario.

void bw_DoWork(object sender, DoWorkEventArgs e)
{
// Evento asíncrono que llama al proceso síncrono
int valor = (int)((object[])e.Argument)[0];
BackgroundWorker worker = (BackgroundWorker)sender;
e.Result = new TareaPesada().ConvertirNumero(valor, worker, e);
}

A diferencia de Thread esta vez sí que se permite la devolución de un resultado de la ejecución asíncrona, que podremos rescatar en el evento de finalización.

void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// Aquí se puede rescatar el resultado de la operación con e.Result
this.btnAsincrono.Enabled = true;
this.btnCancelar.Enabled = false;
}

Y  para mostrar el progreso del método asíncrono bastará con recuperar el parámetro que nos pasa el evento en forma de “EventArgs”

void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.lblProgreso.Text = (string)e.UserState;
}

BackgrounWorker – Cancelación del proceso y proceso asíncrono

Cancelar el proceso es igual de fácil, tan sólo hay que invocar al método implementado.

private void btnCancelar_Click(object sender, EventArgs e)
{
// Invoca al método que detiene el proceso asíncrono
bw.CancelAsync();
this.btnCancelar.Enabled = false;
this.btnAsincrono.Enabled = true;
}

Y para finalizar el ejemplo el código que controla el proceso asíncrono:

public string ConvertirNumero(int x, BackgroundWorker bw, CancelEventArgs e)
{
detener = false;
int cuenta = 0;
while (cuenta < x)
{
Thread.Sleep(500);
cuenta++;
if (!bw.CancellationPending)
{
bw.ReportProgress((int)(cuenta * 100 / x), ObtenerProgreso(x, cuenta));
}
else
{
// Cancela el hilo de ejecución
e.Cancel = true;
break;
}
}
bw.ReportProgress((int)(cuenta * 100 / x), ObtenerProgreso(x, cuenta));

return x.ToString();
}

La clase Task

Para tareas más complejas y sobre todo para la programación paralela el uso de Task es la recomendación de Microsoft y por el momento la más actual.

Task – Implementación

Nos permite encapsular en la misma clase un método síncrono como asíncrono mediante el uso de las instrucciones “async” y “await”.

Si queremos controlar el progreso y la cancelación tendremos que pasárselo como objetos en los parámetros de entrada al método.

public string ConvertirNumero(int x, IProgress<string> progress, CancellationToken ct)
{
int cuenta = 0;
while (cuenta < x)
{
if (progress != null)
{
progress.Report(ObtenerProgreso(x, cuenta));
}

Thread.Sleep(500);
cuenta++;
// Genera una excepción que prefiero no tener que controlar
//ct.ThrowIfCancellationRequested();
if (ct.IsCancellationRequested)
{
break;
}
}
if (progress != null)
{
progress.Report(ObtenerProgreso(x, cuenta));
}
return x.ToString();
}

public async Task<string> ConvertirNumeroProgresoCancelableAsync(int x, IProgress<string> progress, CancellationToken ct)
{
return await Task.Run(() => { return ConvertirNumero(x, progress, ct); });
}

Un método se convierte automáticamente en asíncrono si en su declaración le ponemos delante la instrucción “async”, pero OJO porque dentro de la misma estará esperando a una instrucción “await”.

Task – Llamada asíncrona

Para consumir el método asíncrono, en el ejemplo, también se va a realizar desde otro método asíncrono donde se manipulen los controles de la interfaz de usuario. El concepto cambia, ya que desde el mismo evento del control las tareas que se hagan son paralelas a lo que pase en la interfaz de usuario, ya no hay que incluir unos gestores de eventos para monitorizar los eventos, sino que que hay una clase para cada uso, una clase “Progress” con un delegado para manejar el progreso y otra clase “CancellationTokenSource” para solicitar la cancelación del proceso asíncrono, por lo que habrá que guardar su objeto a nivel global de la clase para poder invocar la cancelación en cualquier otro momento.

#region Variables
CancellationTokenSource cts;
#endregion
//...
private async void btnAsincrono_Click(object sender, EventArgs e)
{
// Prepara los controles de la interfaz de usuaro
btnAsincrono.Enabled = false;
btnCancelar.Enabled = true;

// Prepara la llamada al método asíncrono y los objetos de control del mismo
TareaPesada tar = new TareaPesada();
Progress<string> progreso = new Progress<string>(MostrarProgreso);
cts = new CancellationTokenSource();
string res = await tar.ConvertirNumeroProgresoCancelableAsync(10, progreso, cts.Token);

// Devuelve al estado original a los controls de la interfaz de usuario
btnCancelar.Enabled = false;
btnAsincrono.Enabled = true;
}
private void btnCancelar_Click(object sender, EventArgs e)
{
if (cts != null)
{
cts.Cancel();
}
}
private void MostrarProgreso(string progresoTxt)
{
this.lblProgreso.Text = progresoTxt;
}

Resumen

Thread nos permite hacer invocaciones asíncronas, los inconvenientes principales son, el paso de parámetros no está tipado, para controlar el progreso y resultado se de debe hacer una clase especial (tipo EventArgs) para poder obtener los resultados mediante delegados, ha de usarse también variables de tipo “volatil” para poder controlar el estado y en general es algo complejo.

BackgrounWorker tiene un gran inconveniente, y es si quieres que la clase contenga un método síncrono y otro asíncrono, por ejemplo “ConvertirNumero” y “ConvertirNumeroAsync” su implementación se hace bastante tediosa. Además se hace compleja de manejar en procesos paralelos que necesiten cierto nivel de control sobre el mismo.

Task, como ya dije, es el método recomendado por Microsoft y el que ahora mismo se adapta mejor a las necesidades de los desarrolladores, además de ahorrarnos una buena parte de líneas de código.

Descarga de código fuente

Ficheros de Configuración de Proyecto Adaptables

Seguro que muchos ya conocéis las ventajas que tienen los ficheros de configuración, en los proyectos web, donde los valores se adaptan a la configuración de compilación seleccionada.

Para el que no los conozca recomiendo que visiten la web de MSDN donde lo explican o también el vídeo tutorial en Channel9.

Pero ¿qué ocurre si queremos usarlo en proyectos que no sean web?. De principio, y sin hacer absolutamente nada, no podemos. Pero tenemos tres posibilidades para poder agregar dicha funcionalidad a nuestros proyectos.

Instalación de la Extensión de VS  Configuration Transform

Esta extensión os permitirá agregar ficheros de configuración adaptables al entorno para cualquier proyecto. Yo, desde luego, me decanto por esta opción.

image

Su uso es muy fácil e intuitivo, botón derecho del ratón sobre el fichero de configuración y hacer click en el menú correspondiente.

image

Y si, por ejemplo, añadís nuevos entornos de configuración bastará con volver a hacer “click” en el menú contextual para generar el fichero de configuración y asociarlo al proyecto.

Podéis descargarlo desde aquí.

Instalación del Paquete Nuget ConfigTransform

Este paquete Nuget permite extender el fichero de configuración en el proyecto donde esté instalado. Esta es una buena opción cuando no va a ser muy utilizado en vuestros proyectos y queréis dejar a Visual Studio lo más limpio posible de extensiones. Aunque tiene un PERO muy grande, no es absolutamente nada intuitivo de usar, y yo con el poco tiempo del que dispongo no lo he conseguido. Así que si alguien puede aportar más información será de agradecer.

image

Podéis acceder a su información en este enlace.

Añadirlo de forma manual

Esta es la parte menos sencilla, pero tampoco conlleva mucha complejidad. Se trata de editar el fichero de proyecto (.csproj o .vbproj) y agregarle algunos parámetros para que termine usando la misma librería que se usa en los proyectos web para el fichero “web.config” llamada “Microsoft.Web.Publishing.Tasks.dll”

Primero se debe buscar el nodo XML del fichero de proyecto llamado “ItemGroup” donde esté la entrada al fichero “App.config” y agregar los ficheros de configuración extensibles. Segundo des-comentar el nodo XML “Target” con el nombre “BeforeBuild” y dejarlo como lo pongo a continuación:

<ItemGroup>
    <None Include="App.config">
        <SubType>Designer</SubType>
    </None>
    <None Include="App.Debug.config">
        <DependentUpon>App.config</DependentUpon>
        <SubType>Designer</SubType>
    </None>
    <None Include="App.Release.config">
        <DependentUpon>App.config</DependentUpon>
        <SubType>Designer</SubType>
    </None>
</ItemGroup>
<!--...-->
<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="BeforeBuild">
    <TransformXml Source="App.config" Destination="$(OutputPath)$(TargetFileName).config" Transform="App.$(Configuration).config" ContinueOnError="False" StackTrace="True" />
</Target>

Agregar los fichero de configuración extensibles en el mismo directorio donde está el fichero de configuración del proyecto, acordándose de incluir en el tag XML de “configuration” el elemento “xmlns:xdt” como pongo en el siguiente ejemplo:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    <appSettings>
        <add key="Prueba01" value="TextoARelease" xdt:Transform="Replace" xdt:Locator="Match(key)"/>
    </appSettings>
</configuration>

Volver a cargar el proyecto en Visual Studio y estará listo.


image

Espero que os resulte útil.

Autenticación con Membership ASP.NET Identity

La autenticación Membership tradicional de ASP.NET tiene algunas limitaciones que ahora gracias a WIF (Windows Identity Foundation) se mejora. No voy a entrar en detalles técnicos y en definiciones ya que estoy introduciéndome en esta tecnología y todavía no estoy capacitado para ello.

Por tanto la intención de estos apuntes son los de mostrar un ejemplo de cómo integrar este tipo de autenticación en una aplicación web vacía, ya que las plantilla que VS2013 incluye traer otro módulos que no tienen por qué servirnos y por tanto es muy útil saber qué necesitamos para implementarlo desde cero sin depender de esta plantilla. Se da por supuesto que se tiene una base de datos SQL Server donde poder realizar las pruebas.

Lo primero es instalar mediante NuGet los siquitentes paquetes:

  • Microsoft ASP.NET Identity EntityFramework
  • Microsoft ASP.NET Identity Owin
  • Microsoft.Owin.Host.SystemWeb

Captura

El segundo paso es modificar el web.config. Para ello previamente debemos tener la cadena de conexión con la BBDD sobre la que se van a hacer las pruebas. Dicha base de datos no tiene porqué tener previamente las tablas de Membership generadas, sino que el mismo aplicativo cuando intenta acceder a éste las crea de forma automática.

Por tanto se agrega la cadena de conexión y se incluye el tag “sessionState” indicándole la cadena de conexión a la que debe conectarse.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- ... -->
<connectionStrings>
<add name="DefaultConnection" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=Investigando;Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
<system.web>
<!-- ... -->
<sessionState mode="InProc" customProvider="DefaultSessionProvider">
<providers>
<add name="DefaultSessionProvider" type="System.Web.Providers.DefaultSessionStateProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" />
</providers>
</sessionState>
</system.web>
</configuration>

El tercer paso es agregar una clase que asocie la aplicación con el tipo de autenticación. Para ello hay que incluir en la raiz de la aplicación una clase llamada “Starup.cs”.

image

El código a incluir puede ser el siguiente:

using Microsoft.Owin;
using Owin;

namespace WebApplication2
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
}

Para que funcione también hay que incluir otra clase dentro de la carpeta “App_Start” de la aplicación web:

image

Y que contenta el siguiente código:

using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;

namespace WebApplication2
{
public partial class Startup
{

// Para obtener más información sobre la configuración de la autenticación, visite http://go.microsoft.com/fwlink/?LinkId=301883
public void ConfigureAuth(IAppBuilder app)
{
// Habilitar la aplicación para que use una cookie para almacenar la información del usuario que inició sesión
// y almacenar también información acerca de un usuario que inicie sesión con un proveedor de inicio de sesión de un tercero.
// Es obligatorio si la aplicación permite a los usuarios iniciar sesión
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

// Quitar las marcas de comentario de las líneas siguientes para habilitar el inicio de sesión con proveedores de inicio de sesión de terceros
//app.UseMicrosoftAccountAuthentication(
// clientId: "",
// clientSecret: "");

//app.UseTwitterAuthentication(
// consumerKey: "",
// consumerSecret: "");

//app.UseFacebookAuthentication(
// appId: "",
// appSecret: "");

//app.UseGoogleAuthentication();
}
}
}

Se debe tener en cuenta que se ha cambiado el espacio de nombres y se le ha quitado “App_Start”. Y OJO con las rutas que se le están indicando, se supone que luego se van a crear para la autenticación, lo digo por este código –> LoginPath = new PathString(“/Account/Login”).

El cuarto paso va a consistir en comprobar que efectivamente podemos tener acceso a la base de datos y crear usuarios y roles. En este caso se crea una nueva clase para gestionar las identidades. Como se está usando EntityFramework mediante CodeFirst se generará un contexto de acceso, una clase para manejar los usuarios y un gestor de operaciones sobre los mismos. Este código lo podéis sacar de los proyectos plantilla de ASP.NET. El código para gestionar los roles los he obtenido en el siguiente enlace.

La clase quedaría de la siguiente forma:

image

using System;
using WebApplication2.Models;
using System.Collections.Generic;

namespace WebApplication2.Models
{
// Para agregar datos del usuario, agregue más propiedades a su clase de usuario. Visite http://go.microsoft.com/fwlink/?LinkID=317594 para obtener más información.
public class ApplicationUser : IdentityUser
{
}

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
: base("DefaultConnection")
{
}
}

public class UserManager : UserManager<ApplicationUser>
{
public UserManager()
: base(new UserStore<ApplicationUser>(new ApplicationDbContext()))
{
}
}

public class IdentityManager
{
public bool RoleExists(string name)
{
var rm = new RoleManager<IdentityRole>(
new RoleStore<IdentityRole>(new ApplicationDbContext()));
return rm.RoleExists(name);
}

public bool CreateRole(string name)
{
var rm = new RoleManager<IdentityRole>(
new RoleStore<IdentityRole>(new ApplicationDbContext()));
var idResult = rm.Create(new IdentityRole(name));
return idResult.Succeeded;
}

public bool CreateUser(ApplicationUser user, string password)
{
var um = new UserManager<ApplicationUser>(
new UserStore<ApplicationUser>(new ApplicationDbContext()));
var idResult = um.Create(user, password);
return idResult.Succeeded;
}

public bool AddUserToRole(string userId, string roleName)
{
var um = new UserManager<ApplicationUser>(
new UserStore<ApplicationUser>(new ApplicationDbContext()));
var idResult = um.AddToRole(userId, roleName);
return idResult.Succeeded;
}

public void ClearUserRoles(string userId)
{
var um = new UserManager<ApplicationUser>(
new UserStore<ApplicationUser>(new ApplicationDbContext()));
var user = um.FindById(userId);
var currentRoles = new List<IdentityUserRole>();
currentRoles.AddRange(user.Roles);
foreach (var role in currentRoles)
{
um.RemoveFromRole(userId, role.Role.Name);
}
}
}
}

namespace WebApplication1
{
public static class IdentityHelper
{
// Se utilizan para XSRF al vincular inicios de sesión externos
public const string XsrfKey = "XsrfId";

public static void SignIn(UserManager manager, ApplicationUser user, bool isPersistent)
{
IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = manager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

public const string ProviderNameKey = "providerName";
public static string GetProviderNameFromRequest(HttpRequest request)
{
return request[ProviderNameKey];
}

public static string GetExternalLoginRedirectUrl(string accountProvider)
{
return "/Account/RegisterExternalLogin?" + ProviderNameKey + "=" + accountProvider;
}

private static bool IsLocalUrl(string url)
{
return !string.IsNullOrEmpty(url) && ((url[0] == '/' && (url.Length == 1 || (url[1] != '/' && url[1] != '\\'))) || (url.Length > 1 && url[0] == '~' && url[1] == '/'));
}

public static void RedirectToReturnUrl(string returnUrl, HttpResponse response)
{
if (!String.IsNullOrEmpty(returnUrl) && IsLocalUrl(returnUrl))
{
response.Redirect(returnUrl);
}
else
{
response.Redirect("~/");
}
}
}
}

Aquí tengo que hacer hincapié en que dentro del código hay referencias a rutas de autenticación que van a ser desarrolladas supuestamente a posteriori y que no se incluye en este ejemplo.

Por último queda consumir esta última clase que se ha construido y comprobar que se han creado correctamente el usuario administrador y su rol correspondiente.

Se agrega en Global.asax.cs el siguiente código:

protected void Session_Start(object sender, EventArgs e)
{
// Comprueba que hay un rol de administrador y un usuario asociado
IdentityManager identMgr = new IdentityManager();
if (!identMgr.RoleExists(ROLADM))
{
identMgr.CreateRole(ROLADM);
var user = new ApplicationUser() { UserName = "Administrador" };
if (identMgr.CreateUser(user, "c1@v3~"))
{
identMgr.AddUserToRole(user.Id, ROLADM);
}
}
}

Para terminar comprobando que una vez que se lanza la aplicación se han creado las tablas de forma automática y se ha generado el usuario y rol correspondientes:

image

He realizado un video con todo el proceso completo, pero la calidad del mismo deja mucho que desear debido al escaso tiempo que le he podido dedicar. Pero como una imagen vale más que mil palabras os lo dejo a vuestra disposición, ya que no disponemos de mucha información en este sentido en español.

Ejemplo de implementación de Membership con ASP.NET Identity

 

Un saludo.

Código C# para comprobar permisos de un fichero

Si eres uno de los que está más que harto de no encontrar un código en condiciones para comprobar si un fichero tiene permisos de lectura y escritura entonces has llegado al sitio indicado.

private static bool TienePermisosUsuarioImpersonalizado(string rutaFisica)
{
bool resultado = false;

// Esta variable pretende evitar que salte la excepción de no tener permniso cuando se lanza File.Exists
bool existeRuta = false;

// Obtiene el usuario que lanza la aplicación en la máquina
WindowsIdentity identity = WindowsIdentity.GetCurrent();

try
{
// NOTA : RublenX - Se podría comprobar por FileIOPermission para comprobar File.Exists pero como da una excepción igualmente no se hace así
// Más info en : http://msdn.microsoft.com/es-es/library/vstudio/system.security.permissions.fileiopermission(v=vs.100).aspx
existeRuta = File.Exists(rutaFisica);
}
catch (SecurityException)
{
// Si no hay permisos ni siquiera para acceder al fichero entonces se da por excluido
existeRuta = false;
}

if (!string.IsNullOrEmpty(rutaFisica) && identity != null && existeRuta)
{
// Primero comprueba que el fichero no esté en modo sólo lectura
FileAttributes atributos = File.GetAttributes(rutaFisica);
if ((atributos & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
resultado = false;
}
else
{
// Ahora comprueba que el usuario logado tiene permisos de escritura y lectura
FileSecurity fSecurity = File.GetAccessControl(rutaFisica);
if (fSecurity != null)
{
AuthorizationRuleCollection rules = fSecurity.GetAccessRules(true, true, typeof(SecurityIdentifier));

// Recorre las reglas para todos los usuarios incluidos
foreach (AuthorizationRule rule in rules)
{
FileSystemAccessRule ruleConvertida = rule as FileSystemAccessRule;

if (ruleConvertida != null)
{
// NOTA : RublenX Tal vez se podía haber hecho con Environment.UserName, pero esta forma me gusta mucho más ya que es el usuario que
// ha lanzado la aplicación
if (ruleConvertida.IdentityReference.Value == identity.Owner.Value && ruleConvertida.AccessControlType == AccessControlType.Allow && (ruleConvertida.FileSystemRights & (FileSystemRights.Read | FileSystemRights.Write)) == (FileSystemRights.Read | FileSystemRights.Write))
{
// Una de sus reglas es del usuario que ha lanzado la aplicación con permisos de lectura y escritura
resultado = true;
break;
}
}
}
}
}
}
return resultado;
}

El código es capaz de comprobar si el usuario que lanza la aplicación es capaz de llegar primero al fichero, luego comprueba que no está en modo de sólo lectura, para terminar recorriendo los permisos de ese usuario y ver si son de lectura y escritura.

También puede pasar que necesitéis suplantar el usuario original que ha lanzado la aplicación por otro que tenga permisos de lectura y escritura. En ese caso la siguiente clase os puede ser muy útil:

using System;
using System.Runtime.InteropServices;
using System.Security.Principal;

namespace RublenX.PermisosDeAcceso
{
/// <summary>
/// Clase que permite suplantar el usuario identificado que ejecuta la aplicación en la máquina servidora
/// </summary>
public class SuplantarUsuarioIISBL : IDisposable
{
#region Librerías Externas
public const int LOGON32_LOGON_INTERACTIVE = 2;
public const int LOGON32_PROVIDER_DEFAULT = 0;

WindowsImpersonationContext impersonationContext;

[DllImport("advapi32.dll")]
public static extern int LogonUserA(String lpszUserName,
String lpszDomain,
String lpszPassword,
int dwLogonType,
int dwLogonProvider,
ref IntPtr phToken);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int DuplicateToken(IntPtr hToken,
int impersonationLevel,
ref IntPtr hNewToken);

[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool RevertToSelf();

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern bool CloseHandle(IntPtr handle);
#endregion

#region Métodos Públicos
/// <summary>
/// Inicia la suplantación de usuario por el indicado en los parámetros
/// </summary>
/// <param name="userName">Usuario</param>
/// <param name="domain">Dominio de autenticación</param>
/// <param name="password">Contraseña</param>
/// <returns>True si consigue validar al usuario e impersonalizarlo</returns>
public bool ImpersonateValidUser(String userName, String domain, String password)
{
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(domain))
{
WindowsIdentity tempWindowsIdentity;
IntPtr token = IntPtr.Zero;
IntPtr tokenDuplicate = IntPtr.Zero;

if (RevertToSelf())
{
if (LogonUserA(userName, domain, password, LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT, ref token) != 0)
{
if (DuplicateToken(token, 2, ref tokenDuplicate) != 0)
{
tempWindowsIdentity = new WindowsIdentity(tokenDuplicate);
impersonationContext = tempWindowsIdentity.Impersonate();
if (impersonationContext != null)
{
CloseHandle(token);
CloseHandle(tokenDuplicate);
return true;
}
}
}
}
if (token != IntPtr.Zero)
CloseHandle(token);
if (tokenDuplicate != IntPtr.Zero)
CloseHandle(tokenDuplicate);
}
return false;
}

/// <summary>
/// Devuelve la suplantación del usuario al original
/// </summary>
public void UndoImpersonation()
{
if (impersonationContext != null)
{
impersonationContext.Undo();
impersonationContext.Dispose();
impersonationContext = null;
}
}
#endregion

#region Implementación de la interfaz
/// <summary>
/// Revierte la suplantación de identidad si es que se ha producido y libera todos los recursos utilizados
/// </summary>
public void Dispose()
{
UndoImpersonation();
}
#endregion
}
}

Y por tanto la forma correcta de invocarlo uniendo los dos sería de esta forma:

/// <summary>
/// Comprueba si el fichero tiene permisos de lectura y escritura para el usuario indicado
/// </summary>
/// <param name="rutaFisica">Ruta del fichero</param>
/// <param name="identity">Usuario del que se obtendrán los permiosos (por ejemplo el actual WindowsIdentity.GetCurrent())</param>
/// <returns>Verdadero si tiene los permisos de lectura y escritura</returns>
public static bool TienePermisosLecturaEscritura(string rutaFisica)
{
// Resultado de la validación
bool resultado = false;

// Datos del usuario a autenticar
string u = ConfigurationManager.AppSettings["UsuariosAppLocalAutorizados.LecturaEscrituraDiscoUsuario"];
string d = ConfigurationManager.AppSettings["UsuariosAppLocalAutorizados.LecturaEscrituraDiscoDominio"];
string p = ConfigurationManager.AppSettings["UsuariosAppLocalAutorizados.LecturaEscrituraDiscoPassword"];

using (SuplantarUsuarioIISBL suplantarUsuarioBL = new SuplantarUsuarioIISBL())
{
if (string.IsNullOrEmpty(u))
{
resultado = TienePermisosUsuarioImpersonalizado(rutaFisica);
}
else if (suplantarUsuarioBL.ImpersonateValidUser(u, d, p))
{
resultado = TienePermisosUsuarioImpersonalizado(rutaFisica);
}
else
{
// Personalizar la excepción como os venga en gana
throw new Exception("Error en la aplicación");
}
}

// Devuelve el resultaod de la validación de permisos
return resultado;
}

Espero que os guste.

Como hacer persistente la autenticación en ASP.NET mediante Cookies en Membership

Nota : Me he encontrado entre mis borradores este artículo que no publiqué en su momento, ya está desfasado pero quién sabe si le sirve a alguien por lo que lo publico ahora.

Puede daros el caso como el que yo tengo en el que vuestro proveedor de hosting limite el tiempo máximo de timeout de la sessión a un valor escaso para vuestro fin. Lo cual obliga a vuestros usuarios a tener que autenticarse cada vez que acceden a vuestra web. Para evitar esto y no depender del timeout de sesión podemos hacer persistente la autenticación mediante el uso de Cookies.

Las cookies son ficheros de texto de intercambio entre el explorador y el servidor que almacenan información relevante a la página web y de forma individual para cada usuario. Estos ficheros son almacenados en el equipo que usa el cliente y en algunos casos son objeto de fines malintencionados por lo que evitaremos guardar información confidencial y en nuestro caso almacenaremos un token o símbolo que identifique al usuario.

Cuando el usuario ha sido autenticado correctamente se crea la cookie a guardar en el cliente:

   1: // Crea el contenedor de autenticación
   2: FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1,
   3:                                                                     this.LoginUser.UserName,
   4:                                                                     DateTime.Now,
   5:                                                                     DateTime.Now.AddDays(Constantes.VariablesConfiguracion.AutenticationCookieDaysExpired),
   6:                                                                     true,
   7:                                                                     string.Empty);
   8:
   9: // Encripta the ticket de autenticación
  10: string encTicket = FormsAuthentication.Encrypt(ticket);
  11:
  12: // Crea la cookie
  13: HttpCookie galleta = new HttpCookie(Constantes.AutenticacionCookieName, encTicket);
  14: galleta.Expires = ticket.Expiration;
  15: Response.Cookies.Add(galleta);

Para recuperar la cookie almacenada seguiremos este otro código:

   1: MembershipUser usuarioAutenticado = Membership.GetUser();
   2:
   3: // Comprueba si tiene una cookie almacenada para restaurar la autenticación
   4: if (usuarioAutenticado == null)
   5: {
   6:     // Se comprueba si hay una cookie almacenada
   7:     HttpCookie myCookie = Request.Cookies[Constantes.AutenticacionCookieName];
   8:
   9:     // Lee la cookie y comprueba el usuario y si es válido lo da como autenticado
  10:     if (myCookie != null)
  11:     {
  12:         FormsAuthenticationTicket ticketRecupeado = FormsAuthentication.Decrypt(myCookie.Value);
  13:         string strUsuario = ticketRecupeado.Name;
  14:         MembershipUser usuarioCookie = Membership.GetUser(strUsuario);
  15:         if (usuarioCookie != null && usuarioCookie.IsApproved && !usuarioCookie.IsLockedOut)
  16:         {
  17:             FormsAuthentication.SetAuthCookie(usuarioCookie.UserName, true);
  18:             Response.Redirect(Request.Url.AbsoluteUri);
  19:         }
  20:     }
  21: }

Y eliminamos la cookie cuando el usuario cierra la sesión manualmente.

   1: // Desconecta al usuario activo y elimina sus cookies
   2: HttpCookie myCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
   3: if (myCookie != null)
   4: {
   5:     myCookie.Expires = DateTime.Now;
   6:     Response.Cookies.Set(myCookie);
   7: }

Espero que os funcione.

Como activar la página de errores de ASP.NET

Primero indicar que esto es una MALA PRÁCTICA, pero a veces se hace necesaria cuando no tienes acceso al servidor de hosting como te gustaría, en mi caso no puedo guardar un fichero log de errores, tampoco en base de datos y de hecho no llega ni a pasar por la página inicial, y de depurar en remoto ni hablamos.

La forma más fácil cuando no sabes el error que te da la aplicación es cambiar el web.config y activar la página web de errores que viene por defecto en ASP.NET.

Para ello poner lo siguiente:

<system.web>
<compilation debug="true" targetFramework="4.5">
<customErrors mode="Off" />
</system.web>

Una vez que tengáis el error volver a ponerlo como estuviera, como repito, es una MALA PRÁCTICA.

En mi caso ha sido porque el hosting ha incluido el Framework 4.5 y ahora las configuraciones del web.config son visibles también en los subdominios. Por lo cual la sección correspondiente a las EnterpriseLibrary se estaban pisando y al ser un error del fichero de configuración no pasaba ni tan siquiera por el Global.asax, así que no tenía forma de capturar el error y gracias a esta modificación que comento he podido comprobar el error y arreglarlo.

Detalles del error:

Tipo del Error: Configuration Error 

Descripción: An error occurred during the processing of a configuration file required to service this request. Please review the specific error details below and modify your configuration file appropriately.

Mensaje del Error: Section or group name ‘loggingConfiguration’ is already defined. Updates to this may only occur at the configuration level where it is defined.

Espero que no lo tengáis que utilizar.