11 nov 2011

Listando el contenido de una carpeta o folder

Vamos a implementar un FolderBrowserDialog para mostrar la ventana que se utiliza para cargar el explorador de carpetas de la máquina en una aplicación windows. Lo que vamos a implementar es: escoger una carpeta,  buscar en esta los archivos cuyo tipo calcen con el filtro indicado y listar el resultado.

Para esto vamos añadir los siguientes controles a un windows forms: 


Como puede verse hay dos etiquetas, tres cajas de texto y dos botones.

Por medio del botón Examinar vamos a llamar programáticamente al FolderBrowserDialog con el siguiente código:

private void cmdExaminar_Click(object sender, EventArgs e)
{
    FolderBrowserDialog folderDlg = new FolderBrowserDialog();
    folderDlg.ShowNewFolderButton = false;
    
    DialogResult result = folderDlg.ShowDialog();
    if (result == DialogResult.OK)
    {
        txtpath.Text = folderDlg.SelectedPath;
        Environment.SpecialFolder root = folderDlg.RootFolder;    
    }
}

Como puede verse una vez capturado la ruta de la carpeta seleccionada, ésta se deja en la caja de texto txtPath.

Una vez hecho esto podemos jugar con la caja de texto de filtro para escoger el tipo de documentos a listar y buscamos éstos por medio del siguiente código del botón Buscar:

private void cmdBuscar_Click(object sender, EventArgs e)
{
    DirectoryInfo di = new DirectoryInfo(txtpath.Text);
    FileInfo[] rgFiles = di.GetFiles(txtFiltro.Text);

    StringBuilder sb = new StringBuilder();

    foreach (FileInfo fi in rgFiles)
    {
        sb.AppendLine(fi.Name);

    }
    txtResultado.Text = sb.ToString();
    folderBrowserDialog1.SelectedPath = txtpath.Text;
}

Este código recorre los archivos que se encuentren dentro de la ruta especificada y los muestra en la caja de texto txtResultado, la cual está marcada como multilínea. Recordar utilizar el namespace System.IO  para que funcione todo adecuadamente.

Con este código ya funciona nuestra aplicación: como podemos ver en las siguientes capturas de pantalla:
















Muy útil cuando se requiere listar cierta cantidad de archivos en un correo o en un documento.

24 oct 2011

DataTable Distinct

Tenemos un DataTable con una moderada cantidad de filas, de éstas nos interesan ciertas columnas y que cuyos valores no se repitan: Necesitamos un Distinct. Usando C# ¿Cómo logramos eso?

De momento se me ocurren dos soluciones: LINQ y una técnica usa el  DefaultView  . Sería cuestión de probar cual es más eficiente ya que las dos logran el mismo resultado. Hagamos entonces las prueba.

Primero nos hacemos un proyecto consola y nos creamos un DataTable con unas tres columnas; lo llenamos con valores aleatorios para simular las filas. Esta prueba la realizaremos con 200 mil registros:

//Para llevar el control del tiempo de ejecucion
Stopwatch st = new Stopwatch();

//Creamos la tabla
DataTable dt = new DataTable();
dt.Columns.Add("Codigo", typeof(int));
dt.Columns.Add("Descripcion", typeof(string));
dt.Columns.Add("Ubicacion", typeof(int));

//La llenamos con valores aleatorios
Random x = new Random();
for (decimal i=0; i < 200000; i++)
{
    int X = x.Next(1,100);
    int Y = x.Next(1,10);
    int Z = x.Next(1,1000);
    dt.Rows.Add(X, Y.ToString(), Z);

}

De esta tabla voy a querer las columnas Codigo y Descripcion pero sin registros repetidos. Completamos esta tarea aplicando el distinct según LINQ,

st.Start();
//Linq para extraer las columnas deseadas sin que se repitan
var distinctRows = (from DataRow dRow in dt.Rows
                    select new
                    {
                        col_id = dRow["Codigo"],
                        col_desc = dRow["Descripcion"]
                    }).Distinct();
st.Stop();

//Mostramos la cantidad de registros únicos encontrados y el tiempo de ejecución
string tiempoCarga = (st.ElapsedMilliseconds / 1000).ToString();
Console.WriteLine("Registros {0} cargados en {1} segundos unsando LinQ.",distinctRows.Count().ToString(), tiempoCarga);

Ahora, podemos obtener el mismo resultado  usando el DefaultView del DataTable y vaciandolo en un nuevo DataTable:
st.Reset();//Reseteamos el contador de tiempo
st.Start();
DataTable dt2 = dt.DefaultView.ToTable(true, "Codigo", "Descripcion");
st.Stop();

//Mostramos la cantidad de registros únicos y el tiempo de ejecución
tiempoCarga = (st.ElapsedMilliseconds / 1000).ToString();
Console.WriteLine("Registros {0} cargados en {1} segundos unsando DefaultView.ToTable.",dt2.Rows.Count.ToString(), tiempoCarga);

Console.ReadLine();

Ahora procedemos a ejecutar la prueba y obtener los resultados:

Como podemos observar ambos llegan a la misma cantidad de registros únicos, pero los tiempos de respuesta difieren significativamente uno de otro. Mientras el Distinct de LINQ es prácticamente instantáneo, usando la técnica del  DefaultView consume en promedio 6 segundos. Ganador absoluto de la prueba LINQ!

Solo como referencia final, la forma en la que se recorren los registros obtenidos por medio del LINQ es la siguiente:

double count = 0;
foreach (var row in distinctRows)
{
    count++;
    Console.WriteLine("{0} - ID:{0}  Descripcion {1} ", count.ToString(), row.col_id.ToString(), row.col_desc.ToString());
}

Interesante!

23 ago 2011

BulkCopy carga masiva en una tabla

Tengo un archivo de texto con más de 250 mil líneas. Necesito insertar cada una de éstas como registros en una tabla, el proceso debe realizarse usando C#... Hay una clase interesante dentro del namespace System.Data.SqlClient que nos puede ayudar bastante.

Obviamente en un caso como este el insertar línea por línea en la base de datos queda descartado, sería demasiado ineficiente. Entonces necesitamos tomar conjuntos de registros e insertarlos de un solo golpe. Es aqui donde BulkCopy nos puede salvar.

Para el ejemplo de continuación contamos con una tabla de trabajo que contiene dos columnas, una es un ID y la otra es donde almacenaremos el contenido de cada linea del archivo. La columna ID funcionada como llave. Todo en SQL Server 2005.

Para cargar la información primero necesitamos definir un método por medio de cual leamos el archivo y en memoria lo guardemos en un datatable:

static DataTable TablaTemporal()
{
    string linea = string.Empty;

    double registro = 1;
    DataTable dt = new DataTable();
    DataRow dr = null;
    DataColumn dc = null;

    dc = new DataColumn();
    dc.DataType = System.Type.GetType("System.Double");
    dc.ColumnName = "id";
    dc.Unique = false;
    dt.Columns.Add(dc);

    dc = new DataColumn();
    dc.DataType = System.Type.GetType("System.String");
    dc.ColumnName = "registro";
    dc.Unique = false;
    dt.Columns.Add(dc);

    string filePath = @"\\carpetaDFS\archivoTEST.txt";
    Stopwatch st = new Stopwatch();

    try
    {
        if (File.Exists(filePath))
        {
            st.Start();
            using (StreamReader sr = new StreamReader(filePath))
            {
                while (!sr.EndOfStream)
                {
                    dr = dt.NewRow();
                    linea = sr.ReadLine();
                    dr[0] = registro;
                    dr[1] = linea;
                    dt.Rows.Add(dr);
                    registro += 1;
                }
            }
            st.Stop();
            string tiempoCarga = (st.ElapsedMilliseconds / 1000).ToString();
            Console.WriteLine("Registros cargados en memoria: {0} en {1} segundos.", registro.ToString("N2") , tiempoCarga);
            return dt;
        }
        else
        {
            Console.WriteLine("No fue posible encontrar el archivo: " + filePath);
            return null;
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Se ha producido un error en la creacion de la tabla temporal " + ex.Message);
        return null;
    }
}

Simplemente iteramos por sobre las lineas del archivo y guardamos cada una como un datarow dentro un datatable en memoria. Ahora necesitamos usar esta tabla dentro de un método para carga la información en la base de datos.


static void CargaArchivo()
{
    Stopwatch st = new Stopwatch();
   
    System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("es-CR", false);
    string conexion = "data source=tcp:servidor,puerto; initial catalog=DBDEFAULT; user id=usuario; password=pass";
    SqlConnection conn = new SqlConnection(conexion);

    try
    {
        conn.Open();

        //preparando los parametros del BULK Copy
        SqlBulkCopy bulkCopy = new SqlBulkCopy(conexion, SqlBulkCopyOptions.TableLock);
        bulkCopy.BatchSize = 25000;
        bulkCopy.BulkCopyTimeout = 300;
        bulkCopy.DestinationTableName = "TABLA_DE_TRABAJO"; //Nombre de la tabla en la DB

        //Cargamos una datatable con todas las lineas del archivo por medio de esta funcion
        DataTable tablaCarga = TablaTemporal();


        if (tablaCarga != null)
        {// si la tabla no es nula y tiene filas
            if (tablaCarga.Rows.Count > 0)
            {
                st.Start();
                //Cargamos en la tabla de trabajo
                bulkCopy.WriteToServer(tablaCarga);
                st.Stop();
                string tiempoCarga = (st.ElapsedMilliseconds / 1000).ToString();
                Console.WriteLine("Registros cargados en la tabla de trabajo en {0} segundos.", tiempoCarga );
            }
            else
            {
                Console.WriteLine("El archivo no tiene lineas");
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Se produjo un error: " + ex.Message);
    }
    finally
    {
        if (conn.State == ConnectionState.Open)
        {
            conn.Close();
        }
    }
}

Analicemos el código anterior. Primero establecemos una conexión con nuestra base de datos, la abrimos y seguidamente establecemos los parámetros de nuestro SqlBulkCopy usamos la función creada previamente  para cargar en memoria un datatable con la información del archivo y si esta no devuelve nulo procedemos a cargar en base datos por medio del método WriteToServer.  Al Finalizar cerramos la conexión,

Utilizamos la clase  Stopwatch de System.Diagnostics  para medir el tiempo de procesamiento de nuestro código.


Como se puede apreciar más de 275 mil lineas se cargan en memoria en alrededor de 35 segundos y esta misma cantidad se guardan en la base de datos en 5 segundos. En el archivo que utilizamos cada linea del archivo tiene una longitud de 136 caracteres.

Algo importante a tomar en cuenta es el borrado de la tabla de carga. Probablemente luego de la carga del archivo se dispare algún procedimiento almacenado o algún mecanismo similar para procesar la información cargada en la tabla y una vez finalizado limpiar la tabla para la próxima carga. Lo más eficiente sería usar un TRUNCATE TABLE TABLA_DE_TRABAJO pero se debe tomar en cuenta que el usuario de base de datos debe tener permiso de ALTER sobre la tabla y vigilar el log de transacciones para adaptar su tamaño si se llena.

13 jul 2011

¿Anti-Tecnológico?

Un articulo de opinión
Un amigo por medio de Buzz de google, publicó un enlace a uno de los blogs de BBC Mundo. Este en particular trata sobre el Neulodismo, movimiento o forma de pensar radicalmente anti tecnológica. “Según este movimiento la humanidad estaba mejor en su etapa más primitiva y natural sin contacto con la tecnología.

Ahora bien, como informático y medio tecnófilo, podría parecer que estoy o debo estar absolutamente en contra de semejante idea, pero no.

Alguna vez me he detenido a pensar si realmente todos los avances de la tecnología son provechosos para el ser humano, si de verdad necesitamos tanto gadget , tanta, con perdón de algunos, basura tecnológica:

Hace poco la OMS reconocía una posible relación entre los celulares y algunos tipos de cáncer, ¿Hace cuanto que tenemos un celular en nuestro bolsillo? ¿Y a cuantos conocemos que entran en pánico si olvidaron su iphone4 o su blackberry?¿ A cuantos conoceremos que morirán de cáncer?.

Se supone que ya se alcanzaron los 2 mil millones de personas conectadas a Internet, pero quien dice que Internet no nos "estupidiza". Nicholas Carr en su libro Superficiales ¿qué está haciendo internet con nuestras mentes? Propone que sí lo hace, y ¿quien no se ha percatado de esto? cuando a nuestros niños les ponen un trabajo por insignificante que sea lo bajan de internet y lo presentan tal cual sin siquiera leerlo. Incluso algunos colegas no pueden escribir una línea de código sin acudir a google a buscar el algoritmo que están pretendiendo implementar. Perdemos pensamiento reflexivo y analítico con esta dependencia.

¿Qué pasa con los adictos a Internet? No olvidemos, por poner un ejemplo, la ciber droga que es World of Warcraft que hasta muertos a causado. Los que no puede vivir sin facebook, sin chatear o twitear, sin postear cuanta insignificancia les sucede, poniendo al descubierto su vida privada mucha mas allá de lo sensato . Estas pseudo adicciones ya han requerido la creación de clínicas especializadas incluso para tratar niños.

Ni olvidemos los abusos a menores que se preparan en el anonimato permisivo de las redes sociales, ni las redes de pederastas y pedófilos que proliferan y usan como madriguera a la Internet.

Muy recientemente Sony fue victima de un ataque masivo a su red, y datos sensibles de millones de usuarios aparentemente quedaron a disposición de los atacantes. ¿Cuánta de nuestra información esta “protegida” en la red? ¿Cuán vulnerables somos aun confiando en empresas serias y de trayectoria?

¿Qué sucede si estas en el banco, en el super, en tu oficina y se va la electricidad o se caen los sistemas o simplemente no hay Internet? Caos, anarquía. Dependemos tanto de las computadoras que ni siquiera hay planes de contingencia para cuando no podemos utilizarlas y entonces el mundo se detiene.

¿Cuánta contaminación produce la fabricación, utilización y disposición de nuestra tecnología? ¿Cuál es el impacto en nuestro ambiente? ¿Qué daño produce las cientos de ondas y espectros de frecuencias que utilizamos hoy en día para nuestros wi-fi, 3G, bluetooth,GPS, etc?

¿Realmente tanta tecnología nos ha hecho bien?¿La humanidad, nosotros como personas, somos mejores debido a que tenemos computadoras, tablets, televisores LED 3D, smart phones o internet?

No soy un mal agradecido. La tecnología nos ha hecho avanzar en aspectos claves como la medicina, el trasporte, el entretenimiento,la comunicación, etc, con la potencia de las computadoras desciframos nuestro genoma con la esperanza puesta en el fututo. Las noticias se comunican de forma instantánea, por lo que, por ejemplo, en casos de desastres naturales, la ayuda llega más pronta. Nuestras comunicaciones ya no conocen barreras y compartimos nuestros logros y conocimientos de manera más fluida.

Yo utilizo internet asiduamente, creo que gracias a ella la información esta más disponible que nunca a quién quiera alcanzarla, pero igual puedo buscar cómo optimizar mi consumo de energía y mi vecino cómo construir una bomba casera o un niño acceder a toda clase de filias y aberraciones sexuales. Puedes chatear con tu compañero de trabajo que esta al otro lado del mundo, así como tu hija puede estar entablando una conversación con un depredador sexual que vive en tu misma ciudad.

Al final de mis divagaciones, hoy probablemente estoy más pesimista que de costumbre, solo puedo llegar a la conclusión de que la tecnología es una herramienta, y como tal debemos aprender a utilizarla y enseñar a nuestros hijos como aprovecharla. Quienes las impulsan deben ser responsables de su propio desarrollo y sus consecuencias, asegurar que son completamente seguras antes de su liberación y posterior adopción, que prime el sentido común sobre el dinero, sé que es mucho pedir... el problema de ser idealista.

Vivimos en una vorágine consumista en el que el primer aparatejo que sale al mercado lo adquirimos sin más, hasta vendemos partes de nuestro cuerpo para obtenerlo. No nos interesa de donde viene, si realmente lo necesitamos, lo que sacrificamos para obtenerlo, nada! simplemente queremos estar a la moda.

De cuando en vez necesitamos detenernos un momento mirar al cielo, respirar hondo. Escuchar el canto juguetón de un arroyo y la vida a su alrededor, quizás mientras miramos una puesta de sol, sentados en la hierba y abrazando a nuestros seres queridos, olvidarnos por un instante de la tecnología... necesitamos detenernos un momento y respirar hondo…

8 jun 2011

Construyendo un servicio Windows

Este es un pequeño manual para construir servicios Windows, con recomendaciones de mi propia cosecha sin que esto se acerque a mejores prácticas o algo similar, simplemente anotaciones que para mi son útiles.
Primeramente creamos un proyecto de tipo C# -> Windows y usamos la plantilla Servicio Windows

Luego eliminamos la clase Service1.cs y elegimos agregar un nuevo elemento, y elegimos Servicio de Windows y ponemos el nombre que queremos, en mi caso igual al proyecto TestService

Una vez hecho esto vamos a la clase Program.cs que es de donde realmente se arranca el servicio y don de se instancia el servicio cambiamos Service1 por el nombre que hallamos elegido para nuestro servicio

static void Main()
{
	ServiceBase[] ServicesToRun;
	ServicesToRun = new ServiceBase[];
	{
  		new TestService()
	};
	ServiceBase.Run(ServicesToRun);
}

Ahora bien deseo que mi Servicio Windows tenga algunas llaves parametrizables e instrumentación. Entonces el siguiente paso es agregar un archivo de configuración. Al respecto recordar que el archivo de configuración debe llamarse igual que el servicio más las extensiones ”.exe.config“ o sea, que en nuestro ejemplo seria TestService.exe.config. Para lograr esto añadimos un nuevo elemento tipo Archivo de Configuración de Aplicación

Por cuestiones de requisitos, el webservice debe tener una hora de primera ejecución y luego un intervalo de tiempo. Para esto añadimos dos llaves en el archivo de configuración que acabamos de crear.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key ="HoraEjecucion" value="08:00:00"/>
<add key="FrecuenciaEjecucion" value="30"/>
</appSettings>
</configuration>

La primera hora de ejecución será a la 8 de la mañana y a partir de ahí se ejecutara cada 30 minutos. Ahora necesitamos utilizar estas llaves en nuestra aplicación. Lo primero a tener en cuenta es que si el servicio se inicia a las 5:00 todavía faltaría 3 horas para que el servicio comience a trabajar. Una de las formas de lograr esto es utilizando un Timer, leemos la llave del archivo de configuración las llaves, calculamos el tiempo que falta la primera ejecución, lo colocamos en el intervalo del mismo y lo enlazamos con el evento principal del servicio.

Primero necesitamos tener acceso a las llaves del archivo, entonces añadimos una referencia a la librería de .Net System.configuration y la utilizamos usando dentro de la clase del servicio utilizando el using


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Configuration;
namespace TestService
{
 partial class TestService : ServiceBase
 {
    System.Timers.Timer timer = new System.Timers.Timer();
    public TestService()
    {
        InitializeComponent();
        timer.Enabled = false;
        timer.Elapsed += (evento);
    }

    protected override void OnStart(string[] args)
    {
        PrimeraEjecucion();
    }

    protected override void OnStop()
    {
       /*TODO: agregar código aquí para realizar cualquier anulación necesaria para detener el servicio.*/
    }

    private void evento(object sender, System.Timers.ElapsedEventArgs e)
    {
        MetodoPrincipal();
    }

    protected void MetodoPrincipal()
    {
        timer.Enabled = false;
        /*TODO: Logica Principal del Servicio*/
        SiguienteEjecucion();
    }

    protected void PrimeraEjecucion()
    {
        TimeSpan HoraInicio = TimeSpan.Parse(ConfigurationManager.AppSettings["HoraEjecucion"]);

        TimeSpan TiempoEspera = (System.DateTime.Now.TimeOfDay.Subtract(HoraInicio).Ticks < 0) ?
                 System.DateTime.Now.TimeOfDay.Subtract(HoraInicio).Negate() :
                 HoraInicio.Add(new TimeSpan(1, 0, 0, 0)).Subtract(System.DateTime.Now.TimeOfDay);

        ActualizaTimer(TiempoEspera.TotalMilliseconds);
    }

    protected void SiguienteEjecucion()
    {
        double minutos = Convert.ToDouble(ConfigurationManager.AppSettings["FrecuenciaDeEjecucion"]);
        ActualizaTimer(TimeSpan.FromMinutes(minutos).TotalMilliseconds);
    }

    protected void ActualizaTimer(double milisegundos)
    {
        timer.Enabled = false;
        timer.Interval = milisegundos;
        timer.Enabled = true;
    }
 }
}

La explicación del código de arriba es la siguiente:

Primero creamos el timer de forma global. En la inicialización del componente deshabilitados el timer y, por medio del delegate, le asignamos un evento que apunta al método principal del webservices. Luego en el onStart del servicio llamamos al método PrimeraEjecucion Este se encarga de leer la llave correspondiente a la hora de la primera ejecución del archivo de configuración y calcular cuando hace falta para que esta se alcance (TiempoEspera), lo convierte a milisegundos y con esto llama al método ActualizaTimer que se encarga de añadir el tiempo al Timer y habilitarlo. Con esto conseguimos que una vez que se alcance el tiempo que falta para la primera ejecución el método principal del servicio se ejecute. Finalmente en el método Principal lo primero que hacemos es deshabilitar el timer para no incurrir en llamadas traslapadas si el tiempo de espera para la primera ejecución es muy corto, y al final del mismo llamamos al método SiguienteEjecucion que el intervalo de tiempo de las siguientes ejecuciones del método principal y se lo pasa a ActualizaTimer.

El método SiguienteEjecucion lo podemos modificar un poco de manera que por ejemplo reciba un boolean para saber si la siguiente ejecución es normal o no, previendo que si hay un error en la ejecución del método principal la siguiente ejecución sea en un tiempo distinto a que si terminara de modo normal. Es solo una idea que podríamos implementar.

Ahora queremos implementar la instrumentación, en su faceta de seguimiento o reporte de errores, básicamente habilitar el tracing. Hay varias formas de hacerlo, pero para este caso decidí hacerlo por medio del archivo de configuración usando System.Diagnostics.

Inmediatamente luego de cerrar la sección AppSetttings incluimos la siguiente sección

<system.diagnostics>
<trace autoflush ="true" />
<sources>
 <source name="SourcePrueba" switchName="SwicthPrueba" switchType="System.Diagnostics.SourceSwitch">
   <listeners>
     <clear/>
     <add name="evtlogPrueba"
       type="System.Diagnostics.EventLogTraceListener"
       initializeData="PruebaServicio" />
     <add name="textwriterListener"
       type="System.Diagnostics.TextWriterTraceListener"
       initializeData="Log_ServicioPrueba.txt"
       traceOutputOptions="ProcessId, DateTime, Callstack" />
   </listeners>
 </source>
</sources>
<switches>
 <add name="SwicthPrueba" value="Verbose"/>
</switches>
</system.diagnostics>

Con esto hacemos varias cosas autoflush significa que conforme registremos eventos inmediatamente se incluirán en sus respectivos destinos. El source será lo que instanciamos en la aplicación para implementar el tracing. Este source, a su vez, utiliza el switch especificado para determinar que se va a registrar; esto lo veremos más claramente más adelante. Además incluimos dos listeners uno que es un eventlog y otro que es un archivo de texto. En el que es de tipo eventlog el initializeData se refiere al source del eventlog. Finalmente definimos un switch el cual por, medio de su value, determinara que se registrará cuando usemos el TraceSource en la aplicación.

Quizás todo lo anterior es un poco confuso ya que es la mera configuración del tracing. Ahora lo veremos como utilizarlo dentro de nuestro servicio. Primeramente instanciamos un TraceSouce de manera global (en la misma área que instanciamos el timer)

TraceSource ts = new TraceSource("SourcePrueba");

Como vemos lo instanciamos utilizando como parámetro el mismo nombre que usamos para el source que definimos en la configuración. Ahora vamos ha utilizarlo en varias partes de nuestro código. Por ejemplo vamos a registrar un evento cuando el servicio se inicie y cuando se detenga, modificando los respectivos métodos de la siguiente manera.

protected override void OnStart(string[] args)
{
	ts.TraceEvent(TraceEventType.Information,0,"Servicio Test Iniciado");
	PrimeraEjecucion();
}

protected override void OnStop()
{
	ts.TraceEvent(TraceEventType.Information,0,"Servicio Test Finalizado");
}

El método principal lo vamos a modificar de la siguiente manera para probar como funciona el switch del archivo de configuración al final del presente manual.

protected void MetodoPrincipal()
{
	timer.Enabled = false;
	ts.TraceEvent(TraceEventType.Information, 0, "Servicio Test: Informacion");
	ts.TraceEvent(TraceEventType.Error, 0, "Servicio Test: Error");
	ts.TraceEvent(TraceEventType.Warning, 0, "Servicio Test: Alerta");
	ts.TraceEvent(TraceEventType.Verbose, 0, "Servicio Test; General/Siguimiento");
	/*TODO: Logica Principal del Servicio*/
	SiguienteEjecucion();
} 

Prácticamente hemos finalizado, la creación de nuestro servicio de Prueba. Sin embargo nos falta prepararlo para poder instalarlo. Primero necesitamos añadirle un instalador a nuestro proyecto. Vamos a la clase TestService.cs, pero no al código si no al diseñador, en éste hacemos clic derecho y elegimos Agregar Instalador.

Elegimos la nueva clase que se nos ha añadido ProjectInstaller.cs en su diseñador y escogemos el componente ServiceInstaller1 nos vamos a propiedades y cambiamos algunas propiedades

Como se aprecia, podemos cambiar la descripción del servicio y el nombre que mostrará en la consola de servicios de windows. Así como si quedará con activación automática o manual. Lo configuramos según nos parezca.

Finalmente debemos decidir con que cuenta se ejecutará el servicio para esto seleccionados el serviceProcessInstaller y en sus propiedades buscamos el elemento Account. Se seleccionamos según se desee (Por lo general los servicios se ejecutaran con LocalSystem)

Ahora vamos a crear dos archivos .bat para instalación y desinstalación de nuestro servicio. Esto es muy cómodo por ejemplo, cuando necesitamos pasarlo a otro departamento para su instalación. Estos archivos se puede crear utilizando notepad y luego de guardarlo simplemente le cambiamos la extensión de .txt a .bat

El archivo Instalador.bat quedaría como sigue:

@ECHO OFF

REM Estos es para usar con el Framework .NET 2.0

set DOTNETFX2=%SystemRoot%\Microsoft.NET\Framework\v2.0.50727

set PATH=%PATH%;%DOTNETFX2%

echo Instalando Servicio de Prueba...

echo ---------------------------------------------------

InstallUtil /i TestService.exe

echo ---------------------------------------------------

echo Instalacion Finalizada.

echo ---------------------------------------------------

pause

Y el Desinstalar.bat así:

@ECHO OFF

REM Estos es para usar con el Framework.NET 2.0

set DOTNETFX2=%SystemRoot%\Microsoft.NET\Framework\v2.0.50727

set PATH=%PATH%;%DOTNETFX2%

NET STOP "TestService"

echo Desinstalando Servicio de Prueba...

echo ---------------------------------------------------

InstallUtil /u TestService.exe

echo ---------------------------------------------------

echo Hecho

pause

Nos asegurarmos que todo compila, movemos los archivos que necesitamos a una carpeta aparte para realizar nuestras pruebas.

Comencemos ejecutando el archivo Instalador.bat . Si al seleccionar la cuenta del servicio le indicamos user en lugar de LocalSystem, por poner un ejemplo, probablemente nos pida el usuarioy su respectiva contraseña para ejecutar el servicio Windows en la máquina

Vamos a la consola de servicios para verificar que efectivamente se instaló el servicio.

Antes de iniciar el servicio debemos asegurarnos que si vamos a utilizar un eventlog personalizado este debe estar creado y asociado con el source que indicamos en el archivo de configuración previamente. Si no lo hacemos se creará el source, pero asociado con el eventlog default que suele ser el de aplicación. Una vez hecho esta verificación si fuese el caso, lo iniciamos.

Una vez iniciado, si queremos depurarlo lo que hacemos es asociar el proceso desde Visual Studio. Depurar -> Asociar el Proceso, eligiendo en la pantalla que se nos muestra el proceso en cuestión.

Una vez asociado ya podemos depurar

Una vez ejecutado el método principal del servicio verificamos que los eventos se registraron tanto en el archivo como en el eventlog:

Ahora bien, anteriormente no quedó muy claro para que sirve el switch que definimos en el archivo de configuración, pues bien en este momento se están registrando todo los eventos que definidos en el TraceSource porque este switch tiene el valor Verbose, que significa que registre todo, si queremos que únicamente los errores se registren, es tan simple como cambiar el valor del swicth en el archivo de configuración y reiniciar el Servicio, sin alterar en lo mas mínimo nuestra programación. Para probarlo realizamos el siguiente cambio en el archivo de configuración

<add name ="SwicthPrueba" value="Error"/>

Detenemos el servicio, borramos las entradas del eventlog y el archivo. Iniciamos de nuevo el servicio y procedemos a verificar que ahora únicamente se registran los errores.

Esto es todo de momento, hasta aquí este manual de cómo crear un servicio Windows.

24 may 2011

Todas las FK de una DB

Uno de mis mentores, me pasó el siguiente script cuyo propósito es poder mostrar las relaciones tipo foreing key entre las tablas de una base de datos. De manera que con una sola ejecución nos da de forma tabulada la Tabla Hija, la Tabla Padre, la columna hija y la columna padre.
SELECT
child.name AS "Tabla Hija",
parent.name AS "Tabla Padre",
c_colums.name AS "Columna Hija",
p_colums.name AS "Columna Padre"
FROM sys.foreign_key_columns fkc
INNER JOIN sysobjects child ON child.id = fkc.parent_object_id
INNER JOIN sysobjects parent ON parent.id = fkc.referenced_object_id
INNER JOIN syscolumns c_colums ON c_colums.id = fkc.parent_object_id
AND c_colums.colid = parent_column_id
INNER JOIN syscolumns p_colums ON p_colums.id = fkc.referenced_object_id
AND p_colums.colid = fkc.referenced_column_id

Se requiere obviamente los permisos suficientes para acceder a las tablas del sistema.
Es una forma práctica de documentar estas relaciones en caso de que sean necesario. Muchas Gracias Norber Genius Mesen.

21 may 2011

Identity recien insertado

Supongamos que tenemos la siguiente tabla

IF OBJECT_ID ('tb1', 'U') IS NOT NULL
DROP TABLE tb1
GO
CREATE TABLE tb1
(
id int IDENTITY(1,1) PRIMARY KEY,
descripcion varchar(30)
)

Desde una aplicación C# necesitamos realizar la tarea de insertar en la misma y recuperar la llave primaria recién insertada, que como vemos es un identity, o sea, autoincrementa. Valga el presente ejemplo para decir que en lo personal no me gusta nada la utilización de columnas identity y menos como llaves ya que dan una serie de problemas, principalmente a la hora de migrar datos etc, además se tiene la sensación de que se pierde parte del control con las mismas; dicho lo anterior continuamos. Esto se puede afrontar por medio de la implementación de un store procedure como el siguiente:

IF OBJECT_ID ( '[dbo].[inserta_tb1]', 'P' ) IS NOT NULL
DROP PROCEDURE [dbo].[inserta_tb1]
GO
CREATE PROCEDURE [dbo].[inserta_tb1]
@id int Output,
@descripcion varchar(30)
AS
BEGIN TRY
INSERT INTO tb1 (descripcion) values (@descripcion)
set @id = scope_identity();
END TRY
BEGIN CATCH

DECLARE @ErrorMessage NVARCHAR(4000);
DECLARE @ErrorSeverity INT;
DECLARE @ErrorState INT;

--Si hay una trasaccion activa hace rollback
IF @@TRANCOUNT > 0
ROLLBACK
SELECT
@ErrorMessage = ERROR_MESSAGE(),
@ErrorSeverity = ERROR_SEVERITY(),
@ErrorState = ERROR_STATE();

SET @ErrorMessage = 'Se presentó un error en el procedimiento
almacenado [inserta_tb1]: ' + @ErrorMessage

--Enviando el error
RAISERROR ( @ErrorMessage,
@ErrorSeverity,
@ErrorState
);
END CATCH
GO

En SQL server 2005 existen tres opciones cuando se trata de acceder al valor de columnas identity IDENT_CURRENT, @@IDENTITY y SCOPE_IDENTITY.

IDENT_CURRENT: recibe como parámetro un string con el nombre de la tabla (pe. IDENT_CURRENT (‘tb1’) ;) y devuelve para la tabla dada el ultimo identity generado en cualquier sesión y en cualquier scope (ámbito).

@@IDENTITY: devuelve el último identity generado para cualquier tabla dentro de la sesión actual pero sin importar el scope.

SCOPE_IDENTITY: es una función que devuelve el último identity generado para cualquier tabla dentro de la sesión actual y dentro del scope actual.

Ahora bien para entender bien lo del scope, supongamos que tenemos nuestra tabla tb1 y que esta a su vez tuviese un trigger el cual a la hora de inserta en tb1, insertara un registro en otra tabla que también contiene una columna identity. Si al final de insertar en tb1 invocáramos @@IDENTITY nos devolvería el identity generado en el trigger para la segunda tabla, ya que es el último generado, pero si utilizamos SCOPE_IDENTITY() nos devolvería el identity de tb1, esto porque el scope(ámbito) del trigger es distinto al de la instrucción de inserción en tb1 propiamente dicha.

No es recomendable IDENT_CURRENT, porque en un ambiente de alta concurrencia se corre el riesgo de que el identity que me devuelva no sea el generado por mi sesión si no por la de otro usuario que casualmente esta realizando la misma tarea al mismo tiempo.

Finalmente desde C# podemos extraer el valor de identity por medio del siguiente código:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Data;

using System.Data.SqlClient;

namespace Pruebas

{

class Program

{

static void Main(string[] args)

{

string conexion = @"data source=Servidor\Instancia; initial catalog=BDInicial; user id=Usuario; password= ";

SqlConnection conn = new SqlConnection(conexion);

try

{

conn.Open();

SqlCommand comm = new SqlCommand("inserta_tb1",conn);

comm.CommandType= CommandType.StoredProcedure;

SqlParameter id = new SqlParameter ();

id.ParameterName="@id";

id.DbType = DbType.Int16;

id.Direction = ParameterDirection.Output;

comm.Parameters.Add(id);

SqlParameter des = new SqlParameter();

des.ParameterName="@descripcion";

des.DbType= DbType.String;

des.Direction = ParameterDirection.Input;

des.Value = "Descripcion de prueba";

comm.Parameters.Add(des);

comm.ExecuteNonQuery();

Console.WriteLine("El identity de la insercion es {0}", comm.Parameters["@id"].Value.ToString());

}

catch (Exception ex)

{

Console.WriteLine("Se ha producido un Error: {0}", ex.Message);

}

finally

{

if (conn.State != System.Data.ConnectionState.Closed)

{

conn.Close();

}

}

Console.Write("Presione cualquier tecla para continuar...");

Console.ReadKey();

}

}

}

Insisto que en lo personal no me gustan ni recomiendo (como si tuviera la autoridad de recomendar algo jejeje) el uso de columnas tipo identity, es mejor según mi experiencia, tener un mecanismo propio si es que se necesita que los sistemas generen consecutivos.