sábado, 13 de junio de 2015

The Longest Journey en Steam y parche al español

The Longest Journey, juego que salió en el mercado en 1999, es un clásico de la aventura gráfica. Por las grandiosas rebajas Summer Sale de Steam está a 4.99 USD hasta el 22/junio, bastante económico.



Esta guía es para dejarlo en Español, voces y textos gracias a WolfBelmi88.

Probando el juego recién bajado

Al bajarlo y probarlo de una en Windows 8.1 en Español funciona ok, se abre a 640x480 a full screen.

En el juego puedes presionar:

  • F2 salvar
  • F3 cargar
  • F10 salir, luego Y o S

Si al presionar alguna de estas teclas aparece una rectángulo en blanco, son ventanas de Window antiguas, donde lo más seguro es que hay un Aceptar o Cancelar, por lo que puedes presionar S o Y para indicar Si y con N es no.


Parchando el juego al español

Bajar el parche que contiene un instalador, pesa alrededor de 531 MB:

http://www.mediafire.com/download/758487s15wb9kf8/Traduccion+ESP+The+Longest+Journey.exe

Al abrir el instalador puede que indique la ruta por defecto:

C:\Archivos de programa (x86)\Steam\SteamApps\common\The Longest Journey

en mi caso lo cambié por

C:\Program Files (x86)\Steam\steamapps\common\The Longest Journey

Aceptar y esperar que se parche.

Si te pierdes en los pasos, la autora hizo un video explicativo:


Luego de parchado, ir a Steam e iniciar el juego y estará en español.



Bueno, espero les sirva como a mi. Gracias WolfBelmi88.

Más info: http://steamcommunity.com/sharedfiles/filedetails/?id=338452663

viernes, 5 de junio de 2015

Resolución de problemas con Forms Authentication

Herramientas usadas


  • LogParser 2.2
  • Fiddler
  • Microsoft Network Monitor 3.4

Resumen


A menudo, cuando usas Forms Authentication en una aplicación ASP.NET (no aplica para aplicaciones MVC), se da la necesidad de tener a mano una guía de resolución de problemas cuando un request sucede que intermitentemente la aplicación te redirige a la página login de ese mismo sitio. En un ambiente de Desarrollo puedes fácilmente debugear este problema atachando el proceso en el Visual Studio. En ambiente productivo, sin embargo, la tarea se vuelve muy complicada. Para tracear un problema como este, debes logear toda la información que puedas obtener relativa al problema, hasta llegar a la raíz misma de este.

En esta guía, mostraremos un resumen de lo que es el Forms Authentication. Veremos los escenarios en que un usuario puede ser redirigido a la página de login y como capturar datos que sean relevantes para aislar el problema. También veremos como implementar una interfaz IHttpModule para logear información relativa a Forms Authentication.

Resumen de la Autenticación de ASP.NET Forms


La autenticación de formulario permite autenticar un usuario usando tu propio código para mantener el token de autenticación ya sea en una cookie o en la url de la página. La autenticación participa en el ciclo de vida de una página ASP.NET a través de la clase FormsAuthenticationModule. Puedes acceder a la información de la autenticación a través de la clase FormsAuthentication.

Para usar una autenticación de formulario, creas una página login que colecciona credenciales del usuario y que incluyen código para autentificar dichas credenciales. Típicamente configuras la aplicación para que haga un redirect a una página login cuando los usuarios intentan acceder a un recurso protegido, como una página que requiere autenticación. Si las credenciales del usuario son válidas, puedes llamar a métodos de la clase FormsAuthentication para hacer un redirect de vuelta al request que originalmente se solicitó el recurso con el ticket apropiado de autenticación (cookie). Si no quieres hacer un redirect, puedes solo obtener la cookie de autenticación del formulario o setearla. En los request siguientes, el navegador del usuario pasa la cookie de autenticación en el mismo request, el cual hace que se salte la página de login.

Por defecto, la clase FormsAuthenticationModule se agrega al archivo Machine.config. La clase FormsAuthenticationModule administra el proceso de FormsAuthentication.

La siguiente es una entrada del archivo Machine.config:

<httpModule> 
     <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" />           
</httpModule>";

Configuras la autenticación por formulario usando un elemento de configuración. En el caso más simple, tienes una página login. En el archivo de configuración, especificas la URL donde se redirigirán los request no autentificados. Entonces defines las credenciales válidas, en un archivo web.config o en un archivo separado. El siguiente ejemplo muestra una sección de un archivo de configuración que muestra una página login ylas credenciales de autenticación del método Authenticate. Los passwords han sido encriptados usando el método HashPasswordForStoringInConfigFile.

<authentication mode="Forms"> 
     <forms name="MyAuthCookie" loginUrl="/Login.aspx">     
       <credentials passwordFormat="SHA1">        
         <user name="Kim" password="07B7F3EE06F278DB966BE960E7CBBD103DF30CA6" />         
         <user name="John" password="BA56E5E0366D003E98EA1C7F04ABF8FCB3753889"/>         
       </credentials>
     </forms>
</authentication>

Luego de una autenticación ok, el módulo de FormsAuthenticationModule setea el valor de la propiedad User a una referencia del usuario autentifico. El siguiente código de ejemplo muestra como leer con programación la identidad del usuario autentificado con formularios.

String authUser2 = User.Identity.Name;

Una forma conveniente de trabajar con formularios de autenticación es usar controles de ASP.NET membership y ASP.NET login. ASP.NET membership te permite guardar y administrar información del usuario e incluir métodos para autentificar usuarios. Controles de ASP.NET login trabajan en conjunto con ASP.NET membership. Estos encapsulan la lógica para consultar las credenciales del usuario, validar al usuario, recobrar o reemplazar passwords, etc. De echo, los controles de ASP.NET membership y ASP.NET login proveen una capa de abstracción sobre la autenticación de formulario. Esta características reemplazan la mayoría de todos le típico trabajo que generalmente haces cuando tienes que usar autenticación de formularios.

Escenarios


Razones en la que un request se auto redirige a la página login.aspx:

La cookie de autenticación se pierde.

Escenario 1:

Un usuario se logea en el sitio web y en algún punto, el cliente envía un request al servidor y la clase FormsAuthenticationModule no recibe la cookie o no puede leerla.

Escenario 2:

La cookie de autenticación puede perderse cuando el navegador del cliente alcanza el límite de cookies creadas. Por ejemplo en Internet Explorer el límite es 20. Cuando ingresa la cookie número 21, el navegador elimina una cookie de la colección. Si se elimina la cookie .ASPXAUTH, el usuario será redirigido a la página de login en el siguiente request.

Escenario 3:

Luego que el request sale del cliente, hay varias capas que pueden afectar a los paquetes que están siendo enviados. Para determinar si algún dispositivo de red está eliminando la cookie, tienes que capturar una traza de red en el cliente y en el servidor, y entonces buscar en el cuerpo del request si está la cookie. Debes asegurarte que en el request del cliente se envío la cookie y chequear con una traza si se recibió ficha cookie.

Time-out en el ticket de autenticación de formulario

Una cosa que debes tener en mente en las aplicaciones hechas con ASP.NET 2.0 o superiores, es que el timeout de la autenticación de formulario cambió a 30 minutos por defecto. Esto significa que luego de 30 minutos de inactividad, el usuario será reenviado al login (nota: Cada vez que se accede al sitio, se resetea el reloj a 30 minutos - así se cuenta sólo cuando no se está haciendo nada).

Si quieres cambiar el valor del timeout a uno más largo, solo debes cambiar el valor en el web.config local (el valor del timeout está en minutos):

<system.web> 
     <authentication mode="Forms">   
       <forms timeout="50000000"/>                 
     </authentication>
</system.web>

Escenario 4:

La autenticación del formulario puede dar timeout antes que el valor seteado en el archivo de configuración.

Si el ticket de autenticación por formulario es generado manualmente por código, el timeout del ticket sobreescribe el valor seteado en el archivo de configuración. Por lo tanto, si ese valor es menor que el valor del archivo de configuración, el ticket del formulario de autenticación expirará antes y viceversa. Por ejemplo, asumamos que el atributo timeout está seteado a 30 (30 min) en tag Form del archivo Web.config y por código valor de Expiration del ticket está en 20. En este caso, el ticket del formulario de autenticación expiará pasados los 20 minutos y no a los 30 y el usuario tendrá que logearse de nuevo.
Event code: 4005
Event message: Forms authentication failed for the request. Reason: The ticket supplied has expired.

Escenario 5

Una aplicación web ASP.NET 4 usa autenticación de formulario y el visor de eventos dice:

Event code: 4005 
Event message: Forms authentication failed for the request. Reason: The ticket supplied was invalid.

Recopilación de datos y resolución de problemas

Resolución del problema 1:

Puedes determinar si un request no contiene una cookie activando el cookie logging de Microsoft Internet Information Services (IIS). Para hacer esto sigue estos pasos en IIS 6:

-Abre IIS Microsoft Management Console (MMC).
-Botón derecho en el sitio web y luego Properties.
-Botón en el tab Web Site y clic en Enable Logging.
-Asegúrate que el formato del log es W3C Extended Log File Format.
-Clic en Properties. -Clic en Advanced tab y clic en Extended Properties.
-Bajo las Extended Properties, selecciona el checkbox Cookie(cs(Cookie)) y Referer (cs(Referer)). En IIS 7 es similar: Cuando seleccionas el formato "W3C" del combo Format y en "Select Fields" selecciona los 2 checkbox de las cookies.

Luego que el problema ocurre, determina cual es el cliente tiene el problema dado su dirección IP. Filtra el log IIS, con la columna "client IP address".

Nota: Puedes usar Log Parser para formatear los logs del IIS. Baja Log Parser del sitio oficial de Microsoft: http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=24659

Un request con cookies quedará registrado así:

2015-06-02 19:54:41 172.31.2.11 POST /Ventas/TestRequest - 80 - 172.31.17.26 Mozilla/5.0+(Windows+NT+6.1;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/43.0.2357.81+Safari/537.36 Cookie1=Hola;+Cookie2=dyxj/ney47DaZiwEMxUJrw/RvTomKx;+Cookie3=NALDO; http://sitio.cl/ 200 0 0 378

Luego de que tengas la lista de request de un usuario específico, busca por los request de la página login. Ya sabes que hay un redirect a esta página, así que busca por los request que suceden antes de la redirección. Si ves que no hay cookie en dichos request, el cliente no la mandó o esta fue eliminada de la red entre el cliente y el servidor.

Nota: El primer request del usuario no tiene la cookie de autenticación a menos que crees un cookie persistente. El log del IIS sólo muestra las cookies que son recibidas en el request. El primer request que debería tener la cookie de autenticación será la request después que suceda se realice el login de forma correcta.

Resolución del problema 2:

Microsoft Internet Explorer tiene las siguientes limitaciones en cuanto a las cookies (limitaciones RFC 2109):
  • 300 cookies en total
  • 4096 bytes por cookie
  • 20 cookies por cada host o dominio

El artículo oficial es: http://support.microsoft.com/kb/306070

La cookie de autenticación puede perderse si se alcanzó el límite de cookies en el cliente. En Internet Explorer tiene la limitación de 20 cookies. Eso quiere decir que si llega una cookie 21, se sobreescribe la primera. Si la cookie .ASPXAUTH es eliminada, el usuario será redirigido a la página de login en el siguiente request. Puedes usar fiddler para ver los header de los request y response y así saber si el cliente está mandando la cookie o no. Puedes bajar Fiddler 2 de Telerik: http://fiddler2.com/fiddler2/

Ejecuta fiddler en el cliente, elimina las trazas http existentes, accede a tu aplicación que implementa la autenticación de formulario, logeate y observa el tráfico http en fiddler para ver si hay algún cambio de la cookie entre el cliente y el servidor. Luego que capturas el tráfico, si fue ok el login, haz doble clic al request para las respuestas, y en tab Cookie ver que se cree la cookie con Set-Cookie.

Por defecto Internet Explorer puede almacenar un máximo de 20 cookies por dominio. Si un servidor envía más de 20 cookies a un cliente, el navegador automáticamente elimina las cookies más viejas.

Cada cookie consiste en un par nombre-valor. Este par debe ser seguido por valores pares de atributos separados por punto y coma. Este límite ha sido aumentado para simplificar el desarrollo y hosting de aplicaciones web sobre dominios que deben usar cookies. Instalando el update 937143 incrementa el número de cookies que Internet Explorer puede almacenar por cada dominio de 20 a 50. Para más detalles mira este link: http://support.microsoft.com/kb/941495

Resolución del problema 3:

Luego que el request sale de un cliente, hay varias capas que pueden afectar a los paquetes que son enviados como un firewall, proxys o balanceadores de carga. Para determinar si un dispositivo de red está eliminando una cookie, tienes que capturar el tráfico de red entre el cliente y el servidor, y ver como está la cookie en el cuerpo del request. Tienes que ver el cliente y ver si la cookie se envió, y luego en el servidor ver la traza para asegurarte que llegó la cookie.

Request del cliente

Este es un request GET luego que el usuario ha sido autenticado. El ticket de autenticación está marcado en amarillo. Esto confirma que la cookie existe en el lado del cliente. Cuando usas una herramienta de captura de red, como Netmon, verás el tráfico que realmente es enviado a través del adaptador.


Request por el lado del servidor

Cuando ves que el request llegó al servidor, te debes asegurar que llegó la misma información que salió del cliente. Si el servidor no recibe la misma información, necesitas investigar otros dispositivos de la red para determinar donde se eliminó la cookie.

Nota: Ha habido casos de filtros ISAPI que eliminan las cookies. Si confirmas que el servidor web recibió la cookie, pero la cookie no se ve en los Logs del IIS, chequea los filtros ISAPI. Tienes que eliminar los filtros y validar que el problema se resolvió.

Resolución del problema 5:

-Si el escenario involucra una granja de servidores o web farm (un balanceador, distintos servidores, una base de datos, un worker process por cada sitio), los Machinekeys deberían ser los mismo en todos lados. Usa el mismo machinekey para mantener la consistencia entre todos los servidores de la granja:

<machineKey validationKey="87AC8F432C8DB844A4EFD024301AC..." decryptionKey="E001A307CCC8B1ADEA..." validation="SHA1" />

-Compara los valores de los timeout de ambos módulos de autenticación de formulario y el módulo sesión de todos los servidores.

-Compara la versión del archivo System.Web.dll dentro de la carpeta Framework de ASP.NET 4 entre todos los servidores de la granja. Cuando falla con el error: Forms authentication failed for the request. Reason: The ticket supplied was invalid. Esto pasa cuando alguno de los servidores no está en la misma versión de Update del MS .NET framework 4.

-Instala Reliability Update 1 para .NET Framework 4 kb2533523 en el servidor que falta y reiniciar ese servidor. Esto corregirá el problema: http://support.microsoft.com/kb/2533523

Fuente: http://www.iis.net/learn/troubleshoot/security-issues/troubleshooting-forms-authentication

viernes, 17 de abril de 2015

Mi primer libro: MVC 4 para .Net desde cero en Español

Hola a todos. Luego de un año de trabajo, terminé de escribir primer libro técnico: MVC 4 con .Net desde cero: Guía práctica para implementar MVC 4 con C# y Visual Studio 2012/2013 para Amazon Kindle e Impreso. ¡Espero lo compren y les guste!

Valor Kindle: a sólo 9.99 USD

Comprar aquí: http://www.amazon.com/MVC-con-Net-desde-cero-ebook/dp/B00V45GWA2/ref=sr_1_8?ie=UTF8&qid=1429322559&sr=8-8&keywords=mvc+4

Valor Impreso: Chile $15.000. Otros países € 32.20 EUR / US $35.00

Chile: comprar por MercadoLibre: http://articulo.mercadolibre.cl/MLC-426603239-libro-mvc-4-en-espanol-_JM o contactarme en mi correo hernaldog@gmail.com ya que vivo aquí.

Link a la tienda Amazon que te corresponde según tu país:


Otros países (España, Argentina, México, otros): Comprar aquí: http://www.amazon.com/MVC-con-Net-desde-cero/dp/1511532130/ref=sr_1_4?ie=UTF8&qid=1429464182&sr=8-4&keywords=mvc+4+spanish



Tabla de contenidos
1. Introducción 
1.1 Introducción a ASP.NET MVC 
1.2 Creando una aplicación MVC 4 
1.3 Estructura de una aplicación MVC en Visual Studio 
1.4 Ejercicios 
1.5 Resumen 

2. Controladores 
2.1 El rol del controlador 
2.2 Un ejemplo: la famosa tienda de música MVC 
2.3 Un Controlador básico 
2.4 Ejercicios 
2.5 Resumen 

3. Vistas 
3.1 ¿Por qué usar una vista? 
3.2 Seleccionando una Vista 
3.3 Vistas fuertemente tipadas 
3.4 View Model 
3.2 Agregar un Vista 
3.3 El motor de vista Razor 
3.4 Ejercicios 
3.5 Resumen 

4. Modelos 
4.1 El Modelo de Music Store 
4.2 Crear el administrador de almacenamiento 
4.3 Ejercicios 
4.4 Resumen 

5. Los HTML helpers 
5.1 El tag form 
5.2 HTML helpers 
5.3 Otros helpers 
5.4 Rendereando los Helpers 
5.5 Ejercicios 
5.6 Resumen 

6 Membrecía, Autorización y Seguridad 
6.1 Usando el atributo Authorize en un login 
6.2 El atributo Authorize para usarlo con roles 
6.3 Cuando usar roles y cuando usuarios 
6.4 Login externo con OAuth y OpenID 
6.4 Puntos de seguridad en una Web App 
6.5 CustomErrors en el web.config 
6.6 Usando la opción Retail 
6.7 Usar un logging dedicado 
6.8 Resumen de las típicas amenazas y links 
6.9 Ejercicios 
6.10 Resumen 

7 Ajax 
7.1 jQuery 
7.2 Ajax Helpers 
7.3 Validación en el lado del Cliente 
7.4 Detrás los helpers 
7.5 Mejorando el rendimiento de Ajax 
7.6 Ejercicios 
7.7 Resumen 

8 Enrutamiento 
8.1 Localizadores de recursos 
8.2 Introducción al Routing 
8.3 Parámetro para “agarrarlos todos” 
8.4 Múltiples parámetros URL en un segmento 
8.5 Uso del StopRountingHandler e IgnoreRoute 
8.6 Depurando Rutas 
8.7 Más ejemplos de la generación de URLs con la clase Route 
8.8 Personalizar constraints de las rutas 
8.8 Usando Routing con Web Forms 
8.9 Ejercicios 
8.10 Resumen 

9 Web API 
9.1 Definición de una Web API 
9.2 Comenzando un proyecto con Web API 
9.3 Escribiendo un controlador API 
9.4 Configurando Web API 
9.5 Agregando rutas a la Web API 
9.6 Binding de Parámetros 
9.7 Filtros en los request 
9.8 Habilitando Dependency Injection (DI) 
9.9 Explorando una Web API 
9.10 Traceando la aplicación 
9.11 Ejemplo de una Web API 
9.12 Ejercicios 
9.13 Resumen

miércoles, 1 de abril de 2015

Como enviar un CSV de VB6 por .NET y de .NET a Google SpreadSheets

La tarea no es fácil, vamos a generar un CSV en VB6 y de ahí consumir una componente .NET y de ahí pasar a GoogleSpreadSheets. Esto es porque investigando, pasar desde VB6 a GoogleSpreadSheets directamente es medio complicado, por lo que usaremos .NET como "puente".

Usaremos:

  • Visual Studio 2013
  • .Net Framework 2.0.
  • Visual Basic 6

  • Configuración en la cuenta Google


    En la cuenta de correo que usará Google Spreadsheets:
  • Crear un proyecto en: https://console.developers.google.com Y activar la API Drive API.
  • Activar aplicaciones no seguras: https://www.google.com/settings/security/lesssecureapps, esto porque no usaremos OAuth 2.0, sino un acceso directo del código con el correo/clave.

  • Codificar la componente .Net


    Instala Google_Data_API_Setup_*.msi, en mi caso bajé Google_Data_API_Setup_2.2.0.0.msi.
    Las dll quedarán en por defecto en:

    C:\Program Files (x86)\Google\Google Data API SDK\Redist\

    Fuente: https://developers.google.com/google-apps/spreadsheets/.

    Usaremos .Net Framework 2.0, así que en Visual Studio 2013 crear un proyecto tipo Librería de Clases usando esta versión del Framework.

    Referencia a las DLL:

  • Google.GData.Client
  • Google.GData.Documents
  • Google.GData.Extension
  • Google.GData.Spreadsheets

  • Como dije, sácalas de: C:\Program Files (x86)\Google\Google Data API SDK\Redist\

    Configura el proyecto

    En el archivo AssemblyInfo.cs configura:
    [assembly: ClassInterface(ClassInterfaceType.None)] 
    [assembly: AssemblyKeyFileAttribute("C:\\TFS\Google.snk")]  // C#
    [assembly: Guid("837f7063-8219-4f83-abe5-aa4240c7ac4e")]    //en c# automático
    

    En las propiedades del proyecto:

  • Build: Registrar la interoperabilidad COM
  • Aplicación -> Información del ensamblado -> Crear ensamblado visible a travéz de COM


  • Crea una interfaz (para este ejemplo es útil solo si estás consumiendo desde VB6):
    using System;
    using System.Collections.Generic;
    using System.Runtime.InteropServices;
    using System.Text;
    
    namespace GoogleSpread
    {
        [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIDispatch)]
        public interface IFunciones
        {
            
            int SendCVSToGoogle(string ruta, string file);        
        }    
    }
    

    Crea la clase Funciones:
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;
    using System.Runtime.InteropServices;
    using System.Text;
    using Google.GData.Spreadsheets;
    using Google.GData.Client;
    using Google.GData.Documents;
    using System.Configuration;
    using Google.GData.Client.ResumableUpload;
    
    namespace GoogleSpread
    {
    
        [ComSourceInterfaces("GoogleSpread.IFunciones")]
        public class Funciones : IFunciones
        {
    
            public Funciones()
            {
            }
    
            public int SendCVSToGoogle(string ruta, string archivoCVS)
            {
                try
                {
                    //logging...
    
                    DocumentsService service = new DocumentsService("MyDocumentsListIntegration-v1");              
    
                    string user = ConfigurationManager.AppSettings["correo"];
                    string pass = ConfigurationManager.AppSettings["pass"];
                    service.setUserCredentials(user, pass);
    
                    if (user == null || user == String.Empty || pass == null || pass == String.Empty)
                    {
                        throw new Exception("Usuario o clave incorrecta");
                    }
    
                    //logging hasta aquí ok...
    
                    Authenticator authenticator = new ClientLoginAuthenticator("TestApi", Google.GData.Client.ServiceNames.Documents, service.Credentials);
    
                    // Instantiate a DocumentEntry object to be inserted.
                    DocumentEntry entry = new DocumentEntry();
    
                    // Set the document title
                    entry.Title.Text = archivoCVS;
                    entry.IsSpreadsheet = true;
    
                    string rutaFull = ruta + "\\" + archivoCVS;
                    if (!File.Exists(rutaFull))
                        throw new Exception("Archivo ["+ rutaFull +"] no existe.");
    
                    entry.MediaSource = new MediaFileSource(rutaFull, "text/csv");
    
                    Uri createUploadUrl = new Uri("https://docs.google.com/feeds/upload/create-session/default/private/full");
                    AtomLink link = new AtomLink(createUploadUrl.AbsoluteUri);
                    link.Rel = ResumableUploader.CreateMediaRelation;
                    entry.Links.Add(link);
    
                    entry.Service = service;
    
                    ResumableUploader uploader = new ResumableUploader();
    
                    //logging archivo se subirá...
    
                    // Set the handlers for the completion and progress events
                    uploader.AsyncOperationCompleted += new AsyncOperationCompletedEventHandler(OnDone);
                    uploader.AsyncOperationProgress += new AsyncOperationProgressEventHandler(OnProgress);                
    
                    // Start the upload process
                    uploader.InsertAsync(authenticator, entry, new object());
    
                    
                    return 1;
                }
                catch (Exception ex)
                {
                    //logging error
                    return -1;
                }
            }
    
            static void OnDone(object sender, AsyncOperationCompletedEventArgs e)
            {            
                DocumentEntry entry = e.Entry as DocumentEntry;
                //logging subido ok
            }
    
            static void OnProgress(object sender, AsyncOperationProgressEventArgs e)
            {
                int percentage = e.ProgressPercentage;
                //logging subido un tanto %
            }
        }
    }
    

    Crea un proyecto Test, por ejemplo un winForm con un botón.
    Agrega un archivo de configuración App.config con el correo gmail y la clave:

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
       <appSettings>
            <add key="correo" value="correo@gmail.com"/>
            <add key="pass" value="clave"/>
        </appSettings>
    </configuration>
    

    Agrega el código para que hacer clic sobre el botón invoque a la componente .Net y testearla más facilmente. Recuerde referenciar primero el proyecto:

    private void button1_Click(object sender, EventArgs e)
    {
        FuncionesGoogle f = new FuncionesGoogle();
        f.SendCVSToGoogle("c:\\test", textbox2.text);
    }
    

    Prueba la componente, ejecutando el proyecto Test:

    Revisa el Log, si lo implementaste, si se guardó que se subió ok, ve a ver a http://spreadsheet.google.com y ve si se subió:

    Si abres el archivo de prueba se verá el contenido:


    Llamado desde VB6


    NOTA: Asumo que es otro PC.

    Crea una carpeta con permiso de escritura, por ejemplo C:\temp.

    Adentro deja los archivos:

  • GoogleSpread.dll y GoogleSpread.tlb
  • Además las DLL:
    Newtonsoft.Json.dll, Google.GData.AccessControl.dll, Google.GData.Client.dll, Google.GData.Extensions.dll, Google.GData.Spreadsheets.dll, Google.GData.Documents.dll.
    Nota: Estas 6 dll se obtienen al instalar Google_Data_API_Setup_2.2.0.0.msi obtenido de: https://code.google.com/p/google-gdata/downloads/list y que por defecto quedan en: C:\Program Files (x86)\Google\Google Data API SDK\Redist.

  • Además se debe copiar el archivo .config generado que luego lo configuraremos.

  • Referencia al tlb

    Referencia al archivo tlb:


    Seleccionar el archivo .tlb y luego presionar Aceptar.

    Llamar a la función de .Net entregandole una carpeta y un archivo csv:

    Set objNET = New GoogleSpread.FuncionesGoogle
    objNET.SendCVSToGoogle "C:\", "test.csv"
    

    Genera el .exe de salida, llamalo "testvb.exe" y dejalo en C:\temp.

    En la carpeta C:\temp entonces tenemos las 6 dll, el assembly .dll y el .tlb, el archivo tesvb.exe y el .config (con la configuración del correo) que lo debes llamar test.exe.config para que el assembly .Net lo lea correctamente.

    Ahora ejecuta el testvb.exe y cruza los dedos.

    Bueno, espero les sirva.

    jueves, 22 de enero de 2015

    TreeList con Kendo UI for ASP.NET MVC

    Este post va dirigido a los que hemos trabajado con Kendo UI para ASP.NET MVC y no hemos podido hacer un simple TreeList en menos de 1 hora.

    Primero, tenía instalada la versión de Kendo 2014.2.1008 y escribía en mi vista:
    @(Html.Kendo().TreeList
    

    Y arrojaba error ya que no encontraba el .TreeList, luego viendo lo ejemplo de otras versiones me di cuenta que está incluido en la versión 2014.3.1316 por lo que recomiendo bajar esta. Además que actualmente es la última (por lo que averigüé el TreeList se agregó a Kendo en la versión 2014.3.1119).

    La documentación es mucha, de hecho me perdí tanto leer, al menos en Telerik encontré:

    http://docs.telerik.com/kendo-ui/api/javascript/ui/treelist
    http://docs.telerik.com/kendo-ui/aspnet-mvc/helpers/treelist/overview
    http://demos.telerik.com/kendo-ui/treelist/index

    Luego tenemos los ejemplos que vienen en el ZIP del Kendo comercial (pagado):

    - telerik.ui.for.aspnetmvc.2014.3.1316.commercial.zip, para MVC/ASPX, tiene muchos ejemplos, uno de TreeList que es muy complejo quizá para alguien que quiere hacer su primer TreeList.
    - telerik.kendoui.professional.2014.3.1316.commercial.zip, para JQuery, con otros muchos ejemplos pero el código de los controladores se leen de un servicio en línea por ejemplo:
    var service = "http://demos.telerik.com/kendo-ui/service";
    

    Luego de ver mucho código, partí de cero y era más simple de lo que pensaba.

    Primero tenemos una vista que llama a otra vista Header para la carga de los JS/CSS necesario, como JQuery/Kendo:
    <! DOCTYPE html >
    <html>
    <head>
        @Html.Partial("~/HeaderCssJs.cshtml")
    </head>
    
        
    
        <div class="demo-section k-header">
            @(Html.Kendo().TreeList<models .miviewmodel="">()
            .Name("treelist")
            .Columns(columns =>
            {
                columns.Add().Field(e => e.FirstName).Width(220).TemplateId("photo-template");
                columns.Add().Field(e => e.LastName).Width(160);
                columns.Add().Field(e => e.Address).Title("Address");            
            })
            .Filterable()
            .Sortable()
            .DataSource(dataSource => dataSource
                .Read(read => read.Action("Read", "Home"))
                .ServerOperation(false)
                .Model(m =>
                {
                    m.Id(f => f.EmployeeId);
                    m.ParentId(f => f.ReportsTo);
                    m.Field(f => f.FirstName);
                    m.Field(f => f.LastName);
                    m.Field(f => f.ReportsTo);
                })
            )
            .Height(540)
            )
        </div>
    <style>
            .employee-photo {
                display: inline-block;
                width: 40px;
                height: 40px;
                border-radius: 50%;
                background-size: 40px 44px;
                background-position: center center;
                vertical-align: middle;
                line-height: 41px;
                box-shadow: inset 0 0 1px #999, inset 0 0 10px rgba(0,0,0,.2);
            }
    
            .employee-name {
                display: inline-block;
                vertical-align: middle;
                line-height: 41px;
                padding-left: 10px;
            }
        </style>
    </body>
    
    En el Controlador tenemos 3 actions.

    El que carga inicialmente la Vista:
    public ActionResult Index()
    {
        return View();
    }
    
    El Reader que se llama desde el Evento del TreeList:
    public JsonResult Read([DataSourceRequest] DataSourceRequest request)
    {
       var result = GetDetalle().ToTreeDataSourceResult(request);           
       return Json(result, JsonRequestBehavior.AllowGet);
    }
    
    El que carga el enumerador. Lo mejor es que no importa el orden que se hagan los Add, el Tree List de Kendo se encarga de ordenar los hijos:
    private IEnumerable GetDetalle()
    {
        List<MiViewModel> lista = new List< MiViewModel >();
        lista.Add(new MiViewModel () { EmployeeId = 9, FirstName = "xxxxx", LastName = "yyy", hasChildren = false, ReportsTo = 3, Address = "999", Color = "rojo" });
        lista.Add(new MiViewModel () { EmployeeId = 1, FirstName = "naldo", LastName = "Gonz", hasChildren = true, ReportsTo = null, Address = "123", Color = "rojo" });
        lista.Add(new MiViewModel () { EmployeeId = 2, FirstName = "pepe", LastName = "Super", hasChildren = false, ReportsTo = 1, Address = "456", Color = "rojo" });
        lista.Add(new MiViewModel () { EmployeeId = 3, FirstName = "juan", LastName = "Man", hasChildren = false, ReportsTo = null, Address = "789", Color = "verde" });
        return from a in lista
               select a;
    }
    
    En mi caso tengo una carpeta “img” con dos imágenes “rojo.jpg” y “verde.jpg”.

    Por ultimo la clase MiViewModel:
    using System.Web;
    
    namespace Models
    {
        public class MiViewModel
        {
            public MiViewModel() { }       
            public int? EmployeeId { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
            public string Pos { get; set; }
            public string Color { get; set; }
            public string Address { get; set; }
            public bool hasChildren { get; set; }        
            public int? ReportsTo { get; set; }       
        }        
    }
    

    El resultado es:


    Ahora veamos algunas cosillas que podemos agregar. Por ejemplo cambiar el texto que muestra el botón de arriba, en vez de "Add New Record", ponemos "Crear":
    @(Html.Kendo().TreeList<Modelo>()
    .Toolbar(toolbar => toolbar.Create().Text("Crear"))
    
    Como hacer botones propios en el header del TreeList
    En la definición del TreeList:
    .Toolbar(toolbar =>
    {
       toolbar.Custom().Name("botoncrear").Text("Crear");
       toolbar.Custom().Name("mycommand").Text("View X");
    })
    
    Luego en la sección scripts ponemos el handler del botón que lee una propiedad del modelo por ejemplo (en base a la celda seleccionada) y lo muestra en un alert:
    
    
    Como dejar una fila seleccionable (al hacer clic en una celda toda la fila queda seleccionada):
    @(Html.Kendo().TreeList<Modelo>()
      .Selectable(true)
    
    Como obtener dicha fila seleccionada con jQuery:
    var treeList = $("#contenedor").data("kendoTreeList");
    var row = treeList.select();       
    
    Como cambiarle el estilo a una fila seleccionada, por ejemplo seleccionarla y dejarla con estilo oblique (parecido a Italic):
    var treeList = $("#contenedor").data("kendoTreeList");
    var row = treeList.select();            
    row.css('font-style', 'oblique');
    

    Espero les sirva.

    jueves, 29 de mayo de 2014

    Agregar o Editar Binding en IIS con AppCmd.exe o C#

    Hola a todos, he estado jugando con IIS 8 (Windows 8) y me he puesto a ver como agregar y editar un Binding mediante línea de comandos o por código C#.

    Sabemos que un Binding es un elemento que permite configurar información requerida por los request que se quieren comunicar con un sitio Web. Un sitio web en IIS puede tener N bindings. Es especialmente útil cuando quieres que diferentes direcciones de sitios web usen un sitio web (archicos y clases .net) unico. Por ejemplo, un holding de empresas que poseen diferentes URL pero el servidor que las procesa es único.
    Un binding se conforma de: IP:Puerto:HostName

    Vamos por partes, primero creamos un sitio Web vacio y dejamos el binding por defecto:

    Ahora, agregamos otro binding para ese mismo sitio desde el mismo IIS para hacer la prueba de lo facil que es usando Add:

    Ahora empieza lo interesante, vamos agregar un tercer binding por línea de comandos usando appcmd.exe.
    Viendo el sitio oficial de Technet, no dice mucho, da un ejemplo básico de como usarlo en la línea de comandos.
    Ejecuta el Command prompt en Modo Administrador sino con algunas instrucciones te puede dar el error:
    Cannot read configuration file due to insufficient permissions 
    

    Partamos, ejecutando la ayuda del comando SET de appcmd:
    > C:\Windows\System32\inetsrv\appcmd.exe set site "test" /?
    
    Ahí verás las opciones de SET, arriba se ven las opciones para Binding:
    Luego ejecuta:
    > C:\Windows\system32>C:\Windows\System32\inetsrv\appcmd.exe set site /site.name:test /+bindings.[protocol='http',bindingInformation='*:80:mitest']
    

    Donde: test es mi sitio web ya configurado, http es mi protocolo y *:80:mitest es el binding en si: * es todas las IP, 80 es el puerto y mitest es el nombre del nuevo binding.
    Con el + agregas binding a la colección, con el - eliminas.
    No puedes ejecutar directamente > appcmd set ... como en el ejemplo ya que no está en las variables de ambiente pordefecto, así que debes usar la rura completa C:\Windows\inetsrv.

    Ahora ahora todo ok, tenemos 3 binding.

    Si tratamos de modificar un binding por línea de comandos, nos encontramos con el primer problema, no tiene ID (un BindingID), tienes que buscarlo, para luego modificarlo. Si ejecutaste la ayuda de SET, te darás cuenta que no hay algun comando que ayude (al menos no lo encontré).

    Ahora veamos como agregar por linea de comandos pero ejecutado por código C#:
    protected void Button1_Click(object sender, EventArgs e)
    {
     string windir = Environment.GetEnvironmentVariable("windir");
     
     string comando = windir +"\\System32\\inetsrv\\appcmd.exe set site /site.name:test /+bindings.[protocol='http',bindingInformation='*:80:mitest']";
         
     ExecuteCommandAsync(comando);
    }
    
    public void ExecuteCommandSync(object command)
    {
        try
        {
            System.Diagnostics.ProcessStartInfo procStartInfo = new System.Diagnostics.ProcessStartInfo("cmd", "/c " + command);
           
            procStartInfo.RedirectStandardOutput = true;
            procStartInfo.UseShellExecute = false;                
            procStartInfo.CreateNoWindow = true;
            procStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
    
            System.Diagnostics.Process proc = new System.Diagnostics.Process();
            proc.StartInfo = procStartInfo;
            proc.Start();
            
            string result = proc.StandardOutput.ReadToEnd();
            
            Console.WriteLine(result);
            Debug.WriteLine(result);
        }
        catch (Exception objException)
        {
            Debug.WriteLine("Error:" + objException.Message);
        }
    }
    
    public void ExecuteCommandAsync(string command)
    {
        try
        {
            
            Thread objThread = new Thread(new ParameterizedThreadStart(ExecuteCommandSync));
            
            objThread.IsBackground = true;
            
            objThread.Priority = ThreadPriority.Normal;
            
            objThread.Start(command);
        }
        catch (ThreadStartException objException)
        {
            Debug.WriteLine("Error:" + objException.Message);
        }
        catch (ThreadAbortException objException)
        {
            Debug.WriteLine("Error:" + objException.Message);
        }
        catch (Exception objException)
        {
            Debug.WriteLine("Error:" + objException.Message);
            
        }
    }
    
    El problema acá, es un tema muy comentado en foros de ayuda como StackOverflow, ya que da un maldito error de permisos:
    Cannot read configuration file due to insufficient permissions.
    
    Ya sea ejecutando el Visual Studio como administrador, o dando permisos en la carpeta inetsrv no pude sacar este error. Así que no lo dejé ahí y busque otra vía.

    Ya que todo lo anterior no funcionó, queda la alternativa de encontrar alguna clase maestra. Vamos a listar, agregar y modificar via código C# con la clase ServerManager.
    Primero, en tu proyecto en Visual Studio debes referenciar a la DLL: C:\Windows\System32\inetsrv\Microsoft.Web.Administration.dll.
    Lo otro es que el Pool confugurado en el sitio Web en el IIS, en mi caso uso DefaultAppPool, debe estar correctamente configurado lo que es Identity, sino te dará errores de permiso.
    Por ultimo usamos la clase ServerManager donde podemos manipular AppPools, Sitios o Binding.

    Por ejemplo, con ServerManager tenemos todo el control y podemos recorrer los Sitios y los AppPools de IIS:
    using (ServerManager serverManager = new ServerManager())
    {
        if (serverManager.ApplicationPools == null)
            return;
    
        for (int i = 0; i < serverManager.Sites.Count; i++)
        {
            Microsoft.Web.Administration.ApplicationPoolCollection appPoolCollection = serverManager.ApplicationPools[i];
        }
    
        if (serverManager.Sites == null)
            return;
    
        for (int i = 0; i < serverManager.Sites.Count; i++)
        { 
           Microsoft.Web.Administration.BindingCollection bindingCollection = serverManager.Sites[i].Bindings;
        }
    }
    
    Si queremos listar todos los bindings e información de todos los sitios instalados en IIS, es simple:
    public void ListBinding()
    {
        Label1.Text = "";
        using (ServerManager serverManager = new ServerManager())
        {      
            if (serverManager.Sites == null)
                return;
    
            for (int i = 0; i < serverManager.Sites.Count; i++)
            {
                Debug.WriteLine(Environment.NewLine);
                Debug.WriteLine(serverManager.Sites[i].Id.ToString() + " - " + serverManager.Sites[i].Name);
    
                Label1.Text += "\r\n" + "[" + serverManager.Sites[i].Id.ToString() + " - " + serverManager.Sites[i].Name + "]";
    
                Microsoft.Web.Administration.BindingCollection bindingCollection = serverManager.Sites[i].Bindings;
    
                for (int j = 0; j < bindingCollection.Count; j++)
                {
                    Debug.WriteLine(bindingCollection[j].Host +" " + bindingCollection[j].BindingInformation);
    
                    Label1.Text += " Binding (" + j.ToString() + "): " + bindingCollection[j].Host + " " + bindingCollection[j].BindingInformation;                
                }
            }
        }
    }
    
    Este es el código que permite agregar un binding a un sitio dado. En mi caso uso un "keyName" que es el nombre del nuevo elemento (un nuevo sub-sitio que puede cambiar su información de binding). También requiere el ID del sitio Web:
    public void AddBinding(int id, SiteBinding siteBinding, string KeyName)
    {
        using (ServerManager serverManager = new ServerManager())
        {
            if (serverManager.Sites == null)
                return;
    
            for (int i = 0; i < serverManager.Sites.Count; i++)
            {
                if (serverManager.Sites[i].Id == id)
                {
                    Microsoft.Web.Administration.BindingCollection bindingCollection = serverManager.Sites[i].Bindings;
                    
                    Microsoft.Web.Administration.Binding binding = serverManager.Sites[i].Bindings.CreateElement("binding");
    
                    binding["protocol"] = siteBinding.Protocol;
    
                    binding["bindingInformation"] = string.Format(@"{0}:{1}:{2}", siteBinding.IPAddress, siteBinding.Port.ToString(), siteBinding.HostName);
    
                    bool existe = false;
                    for (int j = 0; j < bindingCollection.Count; j++)
                    {
                        if (bindingCollection[j].Host == siteBinding.HostName)
                        {
                            existe = true;
                            break;
                        }
                    }
    
                    if (existe == false)
                    {
                        bindingCollection.Add(binding);
                        serverManager.CommitChanges();
                    }
    
                    if (bindingNameBase.ContainsKey(newKeyName) == false)
                    {
                        bindingNameBase.Add(newKeyName, siteBinding.HostName);
                    }
    
                    
                }
            }
        }
    }
    
    El código para editar un binding existente de un sitio web existente. Uso el hashtable para mantener los IDs de los Binding:
    public void EditBinding(int id, SiteBinding siteBinding, string keyName)
    {
        using (ServerManager serverManager = new ServerManager())
        {
            if (serverManager.Sites == null)
                return;
    
            for (int i = 0; i < serverManager.Sites.Count; i++)
            {
                if (serverManager.Sites[i].Id == id)
                {
                    Microsoft.Web.Administration.BindingCollection bindingCollection = serverManager.Sites[i].Bindings;
    
                    // se elimina el binding
                    Microsoft.Web.Administration.Binding bindingTmp = null;
                    for (int j = 0; j < bindingCollection.Count; j++)
                    {
                        if (bindingCollection[j].Host == bindingNameBase[keyName].ToString())
                        {
                            bindingTmp = bindingCollection[j];                                
                            break;
                        }
                    }
    
                    if (bindingTmp != null)
                    {
                        bindingCollection.Remove(bindingTmp);
    
                        //se crea de nuevo
                        Microsoft.Web.Administration.Binding binding = serverManager.Sites[i].Bindings.CreateElement("binding");
                        binding["protocol"] = siteBinding.Protocol;
                        binding["bindingInformation"] = string.Format(@"{0}:{1}:{2}", siteBinding.IPAddress, siteBinding.Port.ToString(), siteBinding.HostName);
    
                        bool existe = false;
                        for (int j = 0; j < bindingCollection.Count; j++)
                        {
                            if (bindingCollection[j].Host == siteBinding.HostName)
                            {
                                existe = true;
                                break;
                            }
                        }
    
                        if (existe == false)
                        {
                            bindingCollection.Add(binding);
                            serverManager.CommitChanges();
    
                            bindingNameBase[keyName] = siteBinding.HostName;
                        }
                    }                        
                }
            }
        }
    }
    
    Este código se puede refactorizar, (para los más quisquillosos), es solo una muestra de la capacidad de la clase ServerManager.

    En resumen, tenemos:

    Con IIS Manager:
    • Se puede agregar un binding.
    • Se puede modificar un binding.
    • Se puede eliminar un binding.

    Con AppCmd.exe desde Command Promt:
    • Se puede agregar un binding.
    • No se puede modificar un binding de forma fácil (se debe poder pero no lo encontré, si alguien conoce la solución, que lo coloque en los comentarios).
    • Se puede eliminar un binding.

    Con AppCmd.exe desde C#:
    • No se puede hacer nada, da error de permisos. Si alguien conoce la solución, que lo coloque en los comentarios.
    Con ServerManager desde C#:
    • Se puede agregar un binding.
    • Se puede modificar un binding.
    • Se puede eliminar un binding.
    Espero les haya servido. Nos vemos!

    Links útiles:

    miércoles, 26 de febrero de 2014

    Como reducir la Cyclomatic Complexity (Complejidad Ciclomática) de un método

    Este artículo no explica que es la Complejidad Ciclomática o CC, sino como se puede reducir. ¿Porqué reducirla? Permite reducir el riesgo de la mantención del código ante algún cambio principalmente entre otras maravillas.

    Hay mucha teoría acerca de las ventajas de aplicar esto, léelo por tu cuenta por ejemplo en https://es.wikipedia.org/wiki/Complejidad_ciclom%C3%A1tica.

    Visual Studio 2010, 2012 y 2013 tienen un excelente Analizador de Código, donde arroja un warning o error de acuerdo a ciertas reglas. Por ejemplo por defecto indicará un warning cuando la CC sea mayor a 25.
    PHP tiene una herramienta similar llamada PHPLOC.

    Tips en General

    • Crea métodos privados usando nombres claros para el código duplicado.
    • Crea métodos privados con nombres claros que contengan funcionalidades encapsuladas no directamente relacionadas con la lógica de negocio. Cómo por ejemplo:

    1. Acceso a datos
    2. Llamadas a Web services
    3. Código de patrones de diseño, etc.
    4. Crea métodos privados con nombres claros para encapsular cada pieza con lógica de dominio
    5. Refactoriza cada método usando las siguientes reglas:

    A. Realiza validación de campos o datos de entrada primero y devolver un error o lanza excepciones si falla alguna la validación.
    B. Realiza la validación de los datos del negocio y retorna un error o lanza si la validación falla
    C. Recién ahora ejecuta la operación misma del negocio
    D. Agrega comentarios de porqué haces tal cosa y no otra
    E. Aplica las siguientes reglas de oro:

    - No declares las variables al inicio del método, en vez de ello declara las variables en el punto exacto que las necesites.
    - No reutilices la misma variable para múltiples propósitos.
    - Evita las siguientes prácticas que muchos desarrolladores aplican jurando que ellos optimizan el código para mejor performance (los compiladores actuales no lo necesitan). Estas acciones reducen las lecturas pero no dan ninguna mejora de performance:
    - No intentes minimizar el número de llamadas a métodos.
    - De nuevo, no declares las variables al inicio del método, sino en el punto que la requieras.
    - Permite varios returns desde un método, puede ayudar a veces a mejorar el CC.
    - Si usas Using (StreamReader x...) no hagas adentro un x.Close() ya que el método Close() se gatilla automáticamente con el Dispose() del Using.
    - No uses estas formas de evaluar equivalencia:
    - if (10 == n)
    - if (n.Equals(10))
    - No uses string.Concat para concatenar strings, en cambio usa "+".

    Ejemplo

    Con tal de ejemplificar lo anterior, tomemos un caso de una aplicación bancaria:

    Este método realiza el siguiente algoritmo:

    Revisa si el monto solicitado a retirar es menor a la cantidad almacenada en su cuenta corriente
    Si es así, realiza el retiro
    Si no,
      Chequea si tiene línea de crédito para ver si el puede realizar un retiro
    Si no tiene cupo en la línea de crédito, rechaza la solicitud
    Si no,
      Calcula el máximo de dinero que pueda sacar sin necesidad de aprobación de un ejecutivo. Este cálculo depende del tipo de cuenta
      Si el total de dinero en la línea le alcanza, libera el pago
      Si no, iniciar el flujo de aprobación por un ejecutivo

    Este método de negocio usa las propiedades AccountRepository y TransactionHistoryRepository de la clase para realizar el acceso a datos y la propiedad CreditHistoryServiceAgent para invocar a los Web services.
    Este método usa un patrón de diseño estratégicamente para realizar cierta lógica de negocio según cada tipo de cuenta. La instancia se crea usando el patrón Abstract Factory y se aplica el factory correcto usando el tipo cuenta que está cargada en un diccionario.
    Este método también inicializa el flujo de aprobación si se requiere.

    Este es el método principal Withdraw(...) que permite retiro de dinero:
    /// 
    /// Withdraw money from a bank account
    /// 
    /// withdrawal request
    /// A response that indicate the results of the processing of request
    /// Throws when the input request object is null
    /// Throws when the requested account no is invalid
    public TransactionResponse Withdraw(WithdrawRequest request)
    {
        TransactionResponse response = null;
     
        //Check if the request is null
        if (request != null)
        {
            using (TransactionScope transactionScope = new TransactionScope())
            {
                //Get account
                Account account = AccountRepository.GetAccount(request.AccountNo);
     
                //Check if account is null
                if (account != null)
                {
                    //Check if amount is less than the account balance
                    if (request.Amount < account.Balance)
                    {
                        //Reduce the amout from the balance
                        account.Balance -= request.Amount;
                        AccountRepository.Save(account);
     
                        //Create history entry
                        TransactionHistory history = new TransactionHistory();
                        history.TransactionType = (int)TransactionType.Withdraw;
                        history.AccountId = account.AccountId;
                        history.Time = DateTime.Now;
                        history.Amount = request.Amount;
                        TransactionHistoryRepository.Add(history);
     
                        //Return success response
                        response = new TransactionResponse()
                        {
                            ResponseCode = (int)TransactionResponseCodes.Succeeded,
                            ResponseMessage = Resources.TransactionCompleted,
                        };
                    }
                    else
                    {
                        //Check if OD can be issued based for the customer
                        PersonIdentity identify = new PersonIdentity()
                        {
                            FullName = account.Owner.FullName,
                            NationalIdentityCardNo = account.Owner.NationalIdentityCardNo,
                        };
                        bool canIssueOD = CreditHistoryServiceAgent.CanIssueOD(identify);
     
                        if (canIssueOD)
                        {
                            //Process OD
     
                            //Retrieve the amount that can be withdrawn without any approval
                            IAccountStrategyFactory stratergyFactory;
                            if (!accountStratergyFactories.TryGetValue(account.Type, out stratergyFactory))
                            {
                                throw new ArgumentException("Invalid Account Type");
                            }
     
                            IAccountWithdrawalStrategy withdrawalStratergy = stratergyFactory.GetWithdrawalStrategy();
                            decimal withdrawalLimit = withdrawalStratergy.GetODLimitWithoutApproval(account);
     
                            //check if the amount is less than the withdrawal limit
                            if (account.Balance - request.Amount > withdrawalLimit)
                            {
                                //reduce the amount from account balance
                                account.Balance -= request.Amount;
                                AccountRepository.Save(account);
     
                                //create transaction history entry
                                TransactionHistory history = new TransactionHistory();
                                history.TransactionType = (int)TransactionType.Withdraw;
                                history.Time = DateTime.Now;
                                history.Amount = request.Amount;
                                TransactionHistoryRepository.Add(history);
     
                                //Return success response
                                response = new TransactionResponse()
                                {
                                    ResponseCode = (int)TransactionResponseCodes.Succeeded,
                                    ResponseMessage = Resources.TransactionCompleted,
                                };
                            }
                            else
                            {
                                //Initiate approval workflow
                                IWithdrawalApprovalWorkflow workflow = WorkflowFactory.GetWithdrawApprovalWorkflow();
                                workflow.Request = request;
                                workflow.Start();
     
                                //Return pending approval response
                                response = new TransactionResponse()
                                {
                                    ResponseCode = (int)TransactionResponseCodes.PendingApproval,
                                    ResponseMessage = Resources.PendingApproval,
                                };
     
                            }
                        }
                        else
                        {
                            //Return request failed response
                            response = new TransactionResponse()
                            {
                                ResponseCode = (int)TransactionResponseCodes.Failed,
                                ResponseMessage = Resources.FailedDueToBadCreditHistory,
                            };
                        }
                    }
                }
                else
                {
                    throw new ArgumentException("Invalid Account No");
                }
     
                //Mark the transaction to be commited
                transactionScope.Complete();
            }
        }
        else
        {
            throw new ArgumentNullException("request");
        }
     
        return response;
    }
    

    Refactorizando

    1. Elimina el código duplicado. Crea un nuevo método que guarde en TransactionHistoryRepository y lo llamas 2 veces de Withdraw(...):
    private TransactionResponse PerformWithdrawal(WithdrawRequest request, TransactionResponse response, Account account)
    {
        //Reduce the amout from the balance
        account.Balance -= request.Amount;
        AccountRepository.Save(account);
     
        //Create history entry
        TransactionHistory history = new TransactionHistory();
        history.TransactionType = (int)TransactionType.Withdraw;
        history.AccountId = account.AccountId;
        history.Time = DateTime.Now;
        history.Amount = request.Amount;
        TransactionHistoryRepository.Add(history);
     
        //Return success response
        response = new TransactionResponse()
        {
            ResponseCode = (int)TransactionResponseCodes.Succeeded,
            ResponseMessage = Resources.TransactionCompleted,
        };
        return response;
    }
    

    2. Saca la funcionalidad que no está relacionada directamente con la lógica de negocio hacia métodos separados, esto ayuda a mejorar la mantenibilidad del código, por lo tanto se tiene un Withdraw(...) y un PerformWithdrawal(...) con llamadas a otros métodos:
    public TransactionResponse Withdraw(WithdrawRequest request)
    {
        TransactionResponse response = null;
     
        //Check if the request is null
        if (request != null)
        {
            using (TransactionScope transactionScope = new TransactionScope())
            {
                //Get account
                Account account = AccountRepository.GetAccount(request.AccountNo);
     
                //Check if account is null
                if (account != null)
                {
                    //Check if amount is less than the account balance
                    if (request.Amount < account.Balance)
                    {
                        response = PerformWithdrawal(request, response, account);
                    }
                    else
                    {
                        if (CanIssueODBasedOnCreditHistory(account))
                        {
                            //Process OD
     
                            decimal withdrawalLimit = GetODLimitWithoutApproval(account);
     
                            //check if the amount is less than the withdrawal limit
                            if (account.Balance - request.Amount > withdrawalLimit)
                            {
                                response = PerformWithdrawal(request, response, account);
                            }
                            else
                            {
                                response = StartWithdrawApprovalWorkflow(request, response);
                            }
                        }
                        else
                        {
                            //Return request failed response
                            response = GetFailedResponse(response);
                        }
                    }
                }
                else
                {
                    throw new ArgumentException("Invalid Account No");
                }
     
                //Mark the transaction to be commited
                transactionScope.Complete();
            }
        }
        else
        {
            throw new ArgumentNullException("request");
        }
     
        return response;
    }
     
    private TransactionResponse PerformWithdrawal(WithdrawRequest request, 
    TransactionResponse response, Account account)
    {
        //Reduce the amout from the balance
        account.Balance -= request.Amount;
        AccountRepository.Save(account);
     
        CreateWithdrawalHistoryEntry(request, account);
     
        //Return success response
        return GetSuccessResponse(ref response);
    }
    

    3. Crea métodos privados con nombres claros para cada pieza de la lógica del dominio. Esto ayuda a simplificar la lógica y hacer el código mantenible. El método Withdraw(...) se vería así:
    public TransactionResponse Withdraw(WithdrawRequest request)
    {
        TransactionResponse response = null;
     
        //Check if the request is null
        if (request != null)
        {
            using (TransactionScope transactionScope = new TransactionScope())
            {
                //Get account
                Account account = AccountRepository.GetAccount(request.AccountNo);
     
                //Check if account is null
                if (account != null)
                {
                    //Check if amount is less than the account balance
                    if (request.Amount < account.Balance)
                    {
                        response = PerformWithdrawal(request, response, account);
                    }
                    else
                    {
                        response = ProcessAsOD(request, response, account);
                    }
                }
                else
                {
                    throw new ArgumentException("Invalid Account No");
                }
     
                //Mark the transaction to be commited
                transactionScope.Complete();
            }
        }
        else
        {
            throw new ArgumentNullException("request");
        }
     
        return response;
    }
     
    private TransactionResponse ProcessAsOD(WithdrawRequest request, TransactionResponse 
    response, Account account)
    {
        if (CanIssueODBasedOnCreditHistory(account))
        {
            //Process OD
     
            decimal withdrawalLimit = GetODLimitWithoutApproval(account);
     
            //check if the amount is less than the withdrawal limit
            if (account.Balance - request.Amount > withdrawalLimit)
            {
                response = PerformWithdrawal(request, response, account);
            }
            else
            {
                response = StartWithdrawApprovalWorkflow(request, response);
            }
        }
        else
        {
            //Return request failed response
            response = GetFailedResponse(response);
        }
        return response;
    }
    
    

    4. Refactoriza usando las reglas indicadas:
    - Valida la entrada y retorna una excepción si hay error.
    - Valida los datos del negocio y retorna una excepción si hay error.
    - Ejecuta la operación de negocio en si.
    - Comenta lo que creas prudente.
    public TransactionResponse Withdraw(WithdrawRequest request)
    {
        TransactionResponse response = null;
     
        //Validate input parameters
        if (request != null)
        {
            throw new ArgumentNullException("request");
        }
     
        //Perform business logic in a transaction
        using (TransactionScope transactionScope = new TransactionScope())
        {
            //Get account
            Account account = AccountRepository.GetAccount(request.AccountNo);
            if (account != null)
            {
                throw new ArgumentException("Invalid Account No");
            }
     
            if (request.Amount < account.Balance)
            {
                //If the requested amount is less than the balance then 
                //the withdrawal can be performed directly
                response = PerformWithdrawal(request, response, account);
            }
            else
            {
                //Otherwise the request has to be performed as an OD
                response = ProcessAsOD(request, response, account);
            }
     
            //Mark the transaction to be committed
            transactionScope.Complete();
        }
     
        return response;
    }
     
    private TransactionResponse ProcessAsOD(WithdrawRequest request, TransactionResponse 
    response, Account account)
    {
        //Check if the requested customer can receive and OD based on his credit history
        //Reject the request if he has a bad credit history
        if (CanIssueODBasedOnCreditHistory(account))
        {
            return GetFailedResponse(response);
        }
     
        //Check if the requested amount is less than the OD limit for the account type
        //If so perform the request
        //Else initiate the approval workflow to get branch managers approval
        decimal withdrawalLimit = GetODLimitWithoutApproval(account);
        if (account.Balance - request.Amount > withdrawalLimit)
        {
            return PerformWithdrawal(request, response, account);
        }
        else
        {
            return StartWithdrawApprovalWorkflow(request, response);
        }
    }
     
    private TransactionResponse PerformWithdrawal(WithdrawRequest request, 
    TransactionResponse response, Account account)
    {
        //Reduce the amout from the balance
        account.Balance -= request.Amount;
        AccountRepository.Save(account);
     
        CreateWithdrawalHistoryEntry(request, account);
     
        return GetSuccessResponse(ref response);
    }
    

    5. Aplica las siguientes reglas:
    - Declara las variables sólo cuando las usarás y no al inicio del método.
    - No uses la misma variable 2 o más veces.

    Ejemplo, si tenemos un código correctamente escrito:
    public double FindEffortInManDays(RequirementDefinition requirements, 
                           ComplexityFactors complexity, EnvironmentFactors environment)
    {
        double functionPoints = CalculateFunctionPoints(requirements);
        double adjustedFunctionPoints = CalculateAdjustedFunctionPoints(functionPoints, complexity);
        double effortInManDays = CalculateEffort(adjustedFunctionPoints, environment);
        return effortInManDays;
    }
    
    Si aplicamos lo de declarar las variables al inicio:
    public double FindEffortInManDays3(RequirementDefinition requirements, 
                       ComplexityFactors complexity, EnvironmentFactors environment)
    {
        double functionPoints = 0.0;
        double adjustedFunctionPoints = 0.0;
        double effortInManDays = 0.0;
     
        functionPoints = CalculateFunctionPoints(requirements);
        adjustedFunctionPoints = CalculateAdjustedFunctionPoints(functionPoints, complexity);
        effortInManDays = CalculateEffort(adjustedFunctionPoints, environment);
        return effortInManDays;
    }
    
    Mira lo que pasa si cambias de orden una línea como se se ve abajo, el resultado será totalmente distinto, sin que el compilador lo note:
    public double FindEffortInManDays31(RequirementDefinition requirements, 
    ComplexityFactors complexity, EnvironmentFactors environment)
    {
        double functionPoints = 0.0;
        double adjustedFunctionPoints = 0.0;
        double effortInManDays = 0.0;
     
        adjustedFunctionPoints = CalculateAdjustedFunctionPoints(functionPoints, complexity);
        functionPoints = CalculateFunctionPoints(requirements);
        effortInManDays = CalculateEffort(adjustedFunctionPoints, environment);
        return effortInManDays;
    }
    
    Un impacto similar es reutilizando una misma variable. En el ejemplo de abajo, se usa una misma variable para mantener dos cantidades que lógicamente son distintas:
    public double FindEffortInManDays2(RequirementDefinition requirements, 
    ComplexityFactors complexity, EnvironmentFactors environment)
    {
        double functionPoints = CalculateFunctionPoints(requirements);
        functionPoints = CalculateAdjustedFunctionPoints(functionPoints, complexity);
        double effortInManDays = CalculateEffort(functionPoints, environment);
        return effortInManDays;
    }
    
    El siguiente código tiene otro impacto, ya que se reutiliza un misma variable y cambia un orden del código, dando un resultado erróneo sin que el compilador lo delate:
    public double FindEffortInManDays21(RequirementDefinition requirements, 
    ComplexityFactors complexity, EnvironmentFactors environment)
    {
        double functionPoints = CalculateFunctionPoints(requirements);
        double effortInManDays = CalculateEffort(functionPoints, environment);
        functionPoints = CalculateAdjustedFunctionPoints(functionPoints, complexity);
        return effortInManDays;
    }
    

    Otros tips

    1. No intentes bajar el número de llamados a métodos

    El compilador C# puede optimizar el performance a medida que recorre el código, reduciendo el número de llamadas a métodos no ganas performance. Sin embargo, dividiendo un gran método en múltiples llamados a métodos chicos, haces el código mas mantenible y reusable. Lo que si, asegúrate de nombrarlos apropiadamente.

    2. Si usas using (var x = new StreamReader) no hagas adentro un x.Close() ya que este se gatilla automáticamente con el mismo Dispose() del using.
    using( var reader = new StreamReader(file) )
    {
        variable = reader.ReadToEnd();
        reader.Close(); // Esta linea está de más!!!
    }
    
    Una alternativa es cerrarlo manualmente pero sólo si no usas using:
    reader = new StreamReader(file);
    variable = reader.ReadToEnd();
    reader.Close();
    

    3. No uses las siguientes formas de validad igualdad

    if (10 == n)
    if (n.Equals(10))


    Escribe las condiciones como:
    if  (n == 10)

    Sin embargo, a pesar del impacto en la lectura, debemos comparar los string con string.Equals como se muestra abajo:

    if (string.Equals(string1, string2, StringComparison.Ordinal))

    Esto porque .Net aplicará las reglas de cultura específicas para comprar la igualdad de los strings, algo que no pasa si sólo nos limitamos a usar el símbolo "==".
    Ahora, StringComparison.Ordinal podría darle resultado inesperado, ya que se basa en la cultura actual del usuario (navegador del usuario, en caso de una aplicación web).

    Por lo tanto siempre tenemos que ser específicos en el método de comparar cadenas. StringComparison.Ordinal ve los caracteres escondidos incluso los que no se ven en el navegador (como caracteres especiales ejemplo U+200E) y los compara, en cambio StringComparison.InvariantCultureIgnoreCase los ignora.

    Los tipos de comparación son:
    StringComparison.CurrentCulture
    StringComparison.CurrentCultureIgnoreCase
    StringComparison.InvariantCulture
    StringComparison.InvariantCultureIgnoreCase
    StringComparison.Ordinal
    StringComparison.OrdinalIgnoreCase
    

    Una buena explicación está de cada caso en MSDN:
    http://msdn.microsoft.com/en-us/library/system.stringcomparison(v=vs.110).aspx

    4. Usa "+" en vez de string.Concat

    Esto es lo correcto:
    a = b + c + d;

    Ya que si usas esto:
    a = string.Concat(a, b, c);

    Es una optimización que el compilador mismo se encarga de hacer por lo que no es necesario hacerlo de nuevo. Además el símbolo "+" es mas legible que "Concat".

    Este artículo lo hice basándome en este http://ruwandotnet.wordpress.com/2012/03/13/59/ (en inglés)