Autenticación OAuth

Es posible que muchos de vosotros hayáis oído la noticia de que varias de las grandes empresas ya soportan autenticación en sus APIs a través de OAuth. ¿Qué es OAuth y por qué ha venido para quedarse?

OAuth es un protocolo abierto que permite autenticarse a través de las API de distintas compañías como por ejemplo Twitter, Yahoo, Google, etcétera. Hasta hace bien poco uno de los métodos más usados para la autenticación en las mismas era a través de Autenticación Básica con todo lo que ello suponía: el envío de nuestras credenciales con cada petición fácilmente descifrable. Twitter dejará de dar la posibilidad de este tipo de autenticación en el próximo mes.

¿Qué ofrece OAuth?

El objetivo de este método de autenticación es poder acceder a recursos protegidos sin la necesidad de enviar nuestro usuario y contraseña para solicitar los mismos. ¿Cómo es posible? A través de nuestra aplicación será necesario generar una firma que nos identifique gracias a un conjunto de tokens facilitados por el servicio. En este post voy a centrarme en las dos plataformas con las que he estado trabajando estos días: Twitter y Yahoo.

Workflow de OAuth

En esta imagen podemos ver cuáles son los pasos para poder solicitar los recursos protegidos de un servicio. El objetivo fundamental es conseguir un Access token  y un Access Token secret. Con ellos seremos capaces de generar la firma necesaria para obtener los recursos protegidos de nuestros usuarios sin necesidad de enviar sus credenciales en cada petición. Los pasos que debemos seguir son los siguientes:

  1. Creación de una aplicación y obtención de una Consumer key y Consumer Secret.
  2. Solicitar un token temporal.
  3. Autorización del usuario.
  4. Intercambio del token temporal y la autorización del usuario por un Access Token y Access Secret Token.

Creación de una aplicación y obtención de una Consumer key y Consumer secret

Lo primero que necesitamos para comenzar es una Consumer key y Consumer Secret. Ambos se consiguen cuando creamos “simbólicamente” una aplicación en cualquiera de las plataformas de la cual queremos hacer uso de sus servicios. En el caso de Twitter, para crear la aplicación, podemos acceder a la siguiente dirección. Por otro lado, en Yahoo podemos realizar la misma operación a través de este otro enlace. Obviamente, es necesario tener una cuenta para poder darlas de alta. Una vez creada, en ambos casos podemos ver en los detalles la Consumer Key y Consumer Secret generadas:

Solicitar un token temporal

Llegados a este punto es donde comenzamos con la generación de código. Lo primero que necesitamos antes de pedir la autorización del usuario, para recuperar sus datos privados desde nuestra aplicación, es solicitar un token temporal para poder iniciar un proceso de autorización desde la plataforma en cuestión. Para solicitar un token al servicio serán necesarios los siguientes parámetros:

  • oauth_consumer_key: Se obtuvo cuando dimos de alta la aplicación.
  • oauth_signature_method: Se trata del método que utilizaremos para generar la fima. En este post usaré HMAC-SHA1 ya que Twitter solamente soporta este método hasta la fecha, pero también es posible usar PLAINTEXT y RSA-SHA1. Yahoo soporta tanto HMAC-SHA1 como PLAINTEXT.
  • oauth_signature: La firma generada por nuestra aplicación.
  • oauth_nonce: Se trata de un string aleatorio que acompaña a un timestamp.
  • oauth_timestamp: El timestamp está comprendido entre el 1 de Enero de 1970 y la fecha actual y se utiliza de manera conjunta por el servidor con oauth_nonce para verificar que la petición nunca se ha realizado antes y así prevenir ataques.
  • oauth_version: En la actualidad se está utilizando la versión 1.0 de OAuth. Este parámetro es opcional.
  • oauth_callback: Es importante mencionar que es posible utilizar OAuth tanto con aplicaciones de escritorio como aplicaciones web. Es por ello que este parámetro es importante para que el servicio se redirija a esta dirección después de que se autorice el acceso. En el caso de Twitter se puede omitir si la aplicación es de escritorio pero no ocurre lo mismo para el caso de Yahoo, donde debemos especificar este parámetro con el valor oob (Out of bounds).

Estos parámetros serán parte de la cabecera de la petición dentro del apartado Authorization:

private NameValueCollection GetOAuthParameters(string httpMethod, string url, string requestToken = null, string tokenSecret = null, string verifier = null, string callback = null)
{
        var oAuthParameters = new NameValueCollection
                                    {
                                       {"oauth_timestamp",GetTimeStamp()},
                                       {"oauth_nonce",GetNonce()},
                                       {"oauth_version", "1.0"},
                                       {"oauth_signature_method", "HMAC-SHA1"},
                                       {"oauth_consumer_key", _consumerKey},
                                    };

        if (!String.IsNullOrEmpty(requestToken)) oAuthParameters.Add("oauth_token", requestToken);

        if (!String.IsNullOrEmpty(verifier)) oAuthParameters.Add("oauth_verifier", verifier);

        if (!String.IsNullOrEmpty(callback)) oAuthParameters.Add("oauth_callback", callback);
        var signatureBase = GetSignatureBase(httpMethod, NormalizeUrl(url), oAuthParameters);
        var signature = GetSignature(_consumerSecret, signatureBase, tokenSecret);
        oAuthParameters.Add("oauth_signature", signature);

        Debug.WriteLine("oauth_signature {0}", signature);

        return oAuthParameters;
} 

Generar nonce

Se trata de un valor usado sólo una vez y simplemente se nos pide la generación de un string diferente para cada petición en las especificaciones oficiales de OAuth. Una implementación válida podría ser la siguiente:

private static string GetNonce()
{
    return new Random().Next(Int16.MinValue, Int16.MaxValue).ToString("X");
}

Generar timestamp

Como se explicó en la enumeración de los parámetros necesarios, el timestamp debe estar comprendido entre el 1 de Enero de 1970 y la fecha actual.

private static string GetTimeStamp()
{
    return ((long)(DateTime.UtcNow - (new DateTime(1970, 1, 1))).TotalSeconds).ToString();
}

Normalizar la url

Si la dirección a la cual debemos realizar la petición utiliza un puerto distinto al usado por defecto en su protocolo, deberemos asignar el puerto en cuestión a la dirección:

public static string NormalizeUrl(string url)
{
    var uri = new Uri(url);
    var port = string.Empty;

    if (uri.Scheme == "http" && uri.Port != 80 ||
        uri.Scheme == "https" && uri.Port != 443 ||
        uri.Scheme == "ftp" && uri.Port != 20)
        port = string.Format(":{0}", uri.Port);

    return string.Format("{0}://{1}{2}{3}", uri.Scheme, uri.Host, port, uri.AbsolutePath);
}

Creación de la firma

La firma es quizás la parte más compleja de la aplicación ya que está compuesta de una serie de pasos y si cometemos el error en alguno de ellos, el servidor rechazará la petición sin dar más explicación que un signature_invalid.

Firma base

La firma base, tal y como nos indican en las especificaciones, se trata de la concatenación de el http method utilizado, la url a la que hacemos la petición y el resto de parámetros concatenados y ordenados por nombre y valor.

private static string GetSignatureBase(string httpMethod, string url, NameValueCollection oAuthParameters)
{
    var parameters = new Dictionary<string, string>();
    foreach (var key in oAuthParameters.AllKeys)
        parameters.Add(key, oAuthParameters[key]);

    var normalizedParameters = NormalizeParameters(parameters);

    return string.Format("{0}&{1}&{2}", httpMethod, Uri.EscapeDataString(url), Uri.EscapeDataString(normalizedParameters));
}

Firma encriptada y final

Una vez que tenemos los métodos para concatenar y normalizar los parámetros según lo establecido por OAuth, debemos realizar un último paso para obtener la firma que finalmente pasaremos al servicio. En la misma, debemos concatenar primeramente el token  Consumer Secret  y Token Secret si es que lo hubiera con un ampersand(&) entre ellos. Si utilizáramos PLAINTEXT deberíamos utilizar su código correspondiente en Unicode: %26. Si no existiera el segundo integrante, Token Secret, debemos indicar igualmente el ampersand dentro de la cadena.

Por último es necesario hacer uso del algoritmo de hash HMAC-SHA1, generado a partir del Consumer Secret y Token Secret concatenado anteriormente, para cifrar la firma base creada en el apartado anterior. Un método para llevar a cabo estos pasos podría ser el siguiente:

private static string GetSignature(string consumerSecret, string signatureBase, string tokenSecret = null)
{
    var hmacsha1 = new HMACSHA1(Encoding.UTF8.GetBytes(string.Concat(consumerSecret, "&", tokenSecret)));

    var data = Encoding.ASCII.GetBytes(signatureBase);
    var hashData = hmacsha1.ComputeHash(data);

    return Uri.EscapeDataString(Convert.ToBase64String(hashData));
}

Con este conjunto de métodos para crear un nonce, timestamp y la firma podemos realizar básicamente todas las llamadas que nos son necesarias para establecer ese primer contacto, desde que creamos una aplicación hasta que un usuario accede a establecer conexión con sus datos privados a través de la API usando nuestro cliente.

Llamada al servicio para la obtención del token temporal

Una vez lista la cabecera de nuestra petición, ya podemos realizar la llamada para recuperar el token temporal y un token secret que nos serán útiles para el último paso. Dependiendo del servicio con el que queramos contactar, realizaremos la llamada a una dirección u otra facilitadas por la plataforma:

  • Twitter: http://twitter.com/oauth/request_token
  • Yahoo: https://api.login.yahoo.com/oauth/v2/get_request_token.
public Token GetRequestToken(string url)
{
    var parameters = GetOAuthParameters(HttpMethod.Get, url, callback: "oob");

    var oAuthRequest = new OAuthRequest(parameters, url, HttpMethod.Get, proxy: _proxy);

    return RetrieveToken(oAuthRequest.GetResponse());
}

Para realizar las peticiones podemos utilizar bien WebClient o HttpWebRequest.


using System.Text;
using System.Collections.Specialized;
using System.Net;
using System.IO;
using System.Diagnostics;

namespace OAuthTools
{
    public class OAuthRequest
    {
        private readonly HttpWebRequest _request;

        public string GetResponse()
        {
            try
            {
                Debug.WriteLine("{0}: {1}", _request.Method, _request.RequestUri);

                using (var response = _request.GetResponse())
                {
                    var stream = response.GetResponseStream();
                    using (var reader = new StreamReader(stream))
                    {
                        return reader.ReadToEnd();
                    }
                }
            }
            catch (WebException ex)
            {
                if (ex.Response is HttpWebResponse)
                {
                    using (var reader = new StreamReader(ex.Response.GetResponseStream()))
                    {
                        var result = reader.ReadToEnd();
                        return result;
                    }
                }

                throw;
            }
        }

        public OAuthRequest(NameValueCollection parameters, string url, string method, string realm = null, WebProxy proxy = null)
        {
            ServicePointManager.Expect100Continue = false;
            ServicePointManager.UseNagleAlgorithm = false;

            var header = new StringBuilder();

            header.Append("OAuth ");

            if (!string.IsNullOrEmpty(realm)) header.Append("realm="" + realm + "" ");

            for (var i = 0; i < parameters.Count; i++)
            {
                var key = parameters.GetKey(i);
                header.Append(string.Format("{0}="{1}"", key, parameters[key]));

                if (i < parameters.Count - 1)
                    header.Append(",");
            }

            _request = CreateRequestByProxy(url, method, proxy);

            _request.Headers["Authorization"] = header.ToString();

        }

        private static HttpWebRequest CreateRequestByProxy(string url, string method, WebProxy proxy = null)
        {
            var httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
            httpWebRequest.Method = method.ToUpper();

            if (proxy != null) httpWebRequest.Proxy = proxy;

            return httpWebRequest;
        }
    }
}

Si nos fijamos en el constructor de la clase, lo primero que hacemos es deshabilitar la opción Expect 100 Continue. El propósito de esta propiedad es la posibilidad de que la petición pudiera enviar en primer lugar sólo la cabecera con la autorización, para verificar si efectivamente tenemos permiso de realizar la misma. Si la autenticación fallara devolviendo un 417 nos ahorraríamos el envío de los datos y finalizaría la comunicación. Si por otro lado el servidor devolviera el estado 100 (Continue) el cliente, es decir nuestra aplicación .NET, enviaría el resto del mensaje. Al menos Twitter no soporta este comportamiento y debe ser desactivado para evitar errores imprevistos. La segunda propiedad deshabilitada se utiliza para reducir el tráfico y debe ser anulada también por temas de compatibilidad.
Por otro lado, Realm es el nombre del dominio al que va dirigido aunque no es obligatorio incluirlo.

Una vez realizada la llamada con éxito, recuperamos un conjunto de valores donde podemos encontrar el token temporal necesario para el siguiente paso y el token secret para completar el último. Para recuperarlos, he creado un pequeño método llamado RetrieveToken donde almacenaremos en una clase los valores necesarios de la respuesta:

private static Token RetrieveToken(string result)
{
 var values = HttpUtility.ParseQueryString(result);

 var tokens = new Token { Value = values.Get("oauth_token"), SecretToken = values.Get("oauth_token_secret") };

 return tokens;
}

Autorización del usuario

Una vez que ya hemos obtenido el token temporal, ya podemos solicitar autorización al usuario para poder acceder a sus recursos protegidos. Dependiendo del servicio que estemos utilizando, debemos dirigirnos a su página de autorización con el token temporal como query string. Ejemplos de estas páginas son:

  • Twitter:http://twitter.com/oauth/authorize?oauth_token=
  • Yahoo:https://api.login.yahoo.com/oauth/v2/request_auth?oauth_token=
Process.Start(string.Format(url + "{0}", tokens.Value));

El resultado de esta llamada podría ser parecido a lo siguiente:

Si el usuario acepta, en el caso de Twitter se nos facilitará un número y en Yahoo una cadena de caracteres aleatorios. En cualquiera de los dos casos, este nuevo dato se conoce como oauth_verifier y nos servirá junto con el token temporal y el token secret para finalizar el proceso.

Intercambio del token temporal y la autorización del usuario por un Access Token

 

Ya estamos en la recta final y solamente debemos realizar una última petición. Ya tenemos todos los ingredientes para conseguir el Access Token e incluso todas las recetas (métodos) para llevar a cabo dicha petición, por lo que solamente necesitamos pasar los valores a nuestro código. En el caso de Twitter y Yahoo las direcciones a las cuales debemos realizar la petición son las siguientes:

  • Twitter: http://twitter.com/oauth/access_token
  • Yahoo:https://api.login.yahoo.com/oauth/v2/get_token

Es recomendable realizar la misma a través del método POST. Por otro lado, se adjuntan nuevos parámetros que son: token secret, devuelto en la petición junto con el token temporal, oauth_verifier, el cual se nos facilitó cuando dimos permiso a nuestra aplicación en el paso anterior, y el token temporal en sí que será necesario adjuntarlo en esta ocasión en la cabecera junto con los demás parámetros.

public Token GetAccessToken(string url, string requestToken, string requestTokenSecret, string verifier)
{
 var parameters = GetOAuthParameters(HttpMethod.Post, url, requestToken: requestToken, tokenSecret: requestTokenSecret, verifier: verifier);

 var oAuthRequest = new OAuthRequest(parameters, url, HttpMethod.Post, proxy: _proxy);

 return RetrieveToken(oAuthRequest.GetResponse());
}

Como podemos ver, los métodos utilizados son los mismos que para solicitar un token temporal, añadiendo los valores que hasta ahora eran parámetros opcionales utilizando el Framework 4.0 de .NET. Una vez que se realice la petición obtendremos el access token y access token secret con el que podremos generar nuestras claves.

Para finalizar, me gustaría comentar que, debido al tiempo que he empleado en realizar este estudio y análisis de cada una de las partes y de lo mucho que me tuve que pegar con ello, he subido una pequeña aplicación a Codeplex que servirá como utilidad y aprendizaje sobre el funcionamiento de OAuth con un ejemplo completo de todo este proceso. Aún estoy realizando cambios para intentar mejorarla pero podéis acceder a la misma a través de esta dirección.

Espero que haya sido de utilidad.

¡Saludos!

XMLSerializer y XML Attributes

Debido a la gran cantidad de información externa que debemos tratar en nuestras aplicaciones y la gran necesidad de llegar a un acuerdo de comunicación entre distintas tecnologías, hoy me gustaría mostrar cómo es posible pasar información XML a objetos en .NET de una forma eficiente y automática.

Para mostrar un ejemplo que está a la orden del día, he recuperado uno de mis estados de Twitter en formato XML desde su API para poder tratarlo y montar algunos de sus elementos en los objetos de mi aplicación.

<?xml version="1.0" encoding="UTF-8"?>
<status>
  <created_at>Mon May 24 17:24:07 +0000 2010</created_at>
  <id>14636452344</id>
  <text>Status en XML</text>
  <source>&lt;a href=&quot;http://apiwiki.twitter.com/&quot; rel=&quot;nofollow&quot;&gt;API&lt;/a&gt;</source>
  <truncated>false</truncated>
  <in_reply_to_status_id></in_reply_to_status_id>
  <in_reply_to_user_id></in_reply_to_user_id>
  <favorited>false</favorited>
  <in_reply_to_screen_name></in_reply_to_screen_name>
  <user>
    <id>51491702</id>
    <name>Gisela Torres</name>
    <screen_name>0GiS0</screen_name>
    <location>Madrid (Spain)</location>
    <description>ASP.NET, ASP.NET MVC, JQuery,C#, NHibernate, Windows Azure</description>
    <profile_image_url>http://a1.twimg.com/profile_images/881811810/Picture_7_normal.jpg</profile_image_url>
    <url>http://geeks.ms/blogs/gtorres/</url>
    <protected>false</protected>
    <followers_count>235</followers_count>
    <profile_background_color>C6E2EE</profile_background_color>
    <profile_text_color>663B12</profile_text_color>
    <profile_link_color>1F98C7</profile_link_color>
    <profile_sidebar_fill_color>DAECF4</profile_sidebar_fill_color>
    <profile_sidebar_border_color>C6E2EE</profile_sidebar_border_color>
    <friends_count>105</friends_count>
    <created_at>Sat Jun 27 17:04:48 +0000 2009</created_at>
    <favourites_count>0</favourites_count>
    <utc_offset>3600</utc_offset>
    <time_zone>Madrid</time_zone>
    <profile_background_image_url>http://a1.twimg.com/profile_background_images/60722374/free_twitter_designer.jpg</profile_background_image_url>
    <profile_background_tile>false</profile_background_tile>
    <notifications>false</notifications>
    <geo_enabled>false</geo_enabled>
    <verified>false</verified>
    <following>false</following>
    <statuses_count>1401</statuses_count>
    <lang>en</lang>
    <contributors_enabled>false</contributors_enabled>
  </user>
  <geo/>
  <coordinates/>
  <place/>
  <contributors/>
</status>

Si nos fijamos en la estructura que nos ofrece Twitter para representar un status, vemos que el elemento principal se trata del status en sí con una serie de elementos relacionados directamente con él y, como un conjunto, tenemos un elemento compuesto representando al usuario propietario de ese status. Como podéis imaginar podemos obtener claramente dos objetos bien diferenciados de esta respuesta de Twitter.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;

namespace XMLSerializer
{
    [XmlType(TypeName = "status")]
    public class Status
    {
        [XmlElement("id")]
        public long Id { get; set; }

        [XmlElement("text")]
        public string Text { get; set; }

        [XmlElement("source")]
        public string Source { get; set; }

        [XmlElement("in_reply_to_status")]
        public int InReplyToStatus { get; set; }

        [XmlElement("user")]
        public User User { get; set; }
    }
}

Creamos una clase con un atributo del tipo XmlType que indica el nombre del miembro que vamos a almacenar en la misma. Decoramos cada una de sus propiedades con los valores correspondientes en su representación en XML. No es necesario seguir el mismo orden ni recuperar todos los elementos. Si nos fijamos en la propiedad User, vemos que hace referencia al elemento compuesto que vimos anteriormente y, además, que recibe y devuelve un objeto del tipo User declarado también en la aplicación.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;

namespace XMLSerializer
{
    [XmlType(TypeName = "user")]
    public class User
    {
        [XmlElement("id")]
        public long Id { get; set; }

        [XmlElement("name")]
        public string Name { get; set; }

        [XmlElement("screen_name")]
        public string ScreenName { get; set; }

        [XmlElement("location")]
        public string Location { get; set; }
    }
}

Para comprobar que estas clases son compatibles con lo establecido en formato XML, podemos recuperarlo de la siguiente manera:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Serialization;

namespace XMLSerializer
{
    class Program
    {
        static void Main(string[] args)
        {
            var xml = File.ReadAllText("../../XMLs/Status.xml");

            var xmlSerializer = new XmlSerializer(typeof(Status));
            var reader = new StringReader(xml);
            var status = (Status)xmlSerializer.Deserialize(reader);

            Console.WriteLine("Status Id: {0}", status.Id);
            Console.WriteLine("Status text: {0}", status.Text);
            Console.WriteLine("Status source: {0}", status.Source);
            Console.WriteLine("Status in reply to status id: {0}", status.InReplyToStatus);
            Console.WriteLine("User Id: {0}", status.User.Id);
            Console.WriteLine("User name: {0}", status.User.Name);
            Console.WriteLine("User location: {0}", status.User.Location);
            Console.WriteLine("User screen name: {0}", status.User.ScreenName);
            Console.ReadLine();
        }
    }
}

En el caso de Twitter, y en general en todos aquellos recursos que estén relacionados entre sí, podemos conseguir de una forma rápida y a la vez productiva la unión de los distintos elementos que formarán una gran parte del dominio de la aplicación.

Espero que sea de utilidad.

¡Saludos!

Crear secciones personalizadas en el archivo de configuración

Generalmente hacemos el buen uso de utilizar el archivo de configuración para almacenar algunos valores que son necesarios para el funcionamiento de las aplicaciones, pero que pueden cambiar con el paso del tiempo: Cadenas de conexión a base de datos, end points para conectar con servicios web, direcciones de servidores, etcétera.

Si bien es cierto que existen secciones como ConnectionStrings o AppSettings, llegamos a veces a un punto que esto deja de ser elegante hasta el punto de no saber a qué corresponden ciertas claves. Por ello, os propongo la creación de secciones personalizadas donde podamos almacenar de una forma más organizada todo aquello que añadíamos directamente en AppSettings y que pueda tener alguna relación.

En primer lugar vamos a crear una clase que será la que se corresponda con los datos que almacenaremos en nuestra sección personalizada. La misma heredará de la clase ConfigurationSection:


using System.Configuration;

namespace CustomConfigurationSection
{
    public class SmtpSettings : ConfigurationSection
    {
        [ConfigurationProperty("Url", DefaultValue = "smtp.returngis.com", IsRequired = true)]
        public string Url
        {
            get
            {
                return (string)this["Url"];
            }

            set
            {
                this["Url"] = value;
            }
        }

        [ConfigurationProperty("Port", DefaultValue = 80)]
        public int Port
        {
            get
            {
                return (int)this["Port"];
            }

            set
            {
                this["Port"] = value;
            }
        }
    }
}

Como podemos ver, cada propiedad está decorada con el atributo ConfigurationProperty en el cual determinaremos el nombre de la propiedad con la que se corresponde en el archivo de configuración, el valor por defecto, si es requerida la propiedad, etcétera.
Por otro lado, debemos modificar el apartado configSections del archivo de configuración para declarar la nueva sección de la siguiente manera:

<configSections>
 <section name="SmtpSettings" type="CustomConfigurationSection.SmtpSettings, CustomConfigurationSection"/>
</configSections>

Por último añadimos dentro del apartado <configuration> una nueva sección de tipo SmtpSettings donde especificamos los valores necesarios.

<SmtpSettings Url="smtp.test.com" Port="25"/>

Si quisiéramos acceder a estos valores a través de la aplicación podríamos utilizar el objeto SmtpSettings para recuperarlos de la siguiente manera:


using System;
using System.Configuration;

namespace CustomConfigurationSection
{
    class Program
    {
        static void Main()
        {
            var smtpSettings = (SmtpSettings)ConfigurationManager.GetSection("SmtpSettings");

            Console.WriteLine("SMTP:{0} PORT: {1}", smtpSettings.Url, smtpSettings.Port);
            Console.ReadKey();
        }
    }
}

De esta forma, conseguimos obtener los valores desde el archivo de configuración de una forma más estructurada y en tan solo una línea :)

Espero que sea de utilidad.

¡Saludos!

MAD.NUG: Bang Bang! Optimización económica de SQL Server

Este mes desde Mad.Nug vamos a ponernos las pilas para optimizar todo lo posible SQL Server con la ayuda de Pablo Álvarez Doval.
Veremos algunos casos comunes, cómo diagnosticar el estado de salud del servidor, localización de las consultas más costosas, etcétera.

El evento será en las oficinas de Microsoft Ibérica en Madrid.

Map picture

Más información.

¡Saludos!

Visual Web Developer HTML SourceEditor Package’ has failed to load properly (GUID={xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}}

Es posible que en algún momento podamos encontrarnos con el siguiente error en Visual Studio:

Uno de los casos que puede estar sucediendo es que Visual Studio está tomando el mismo idioma que tiene Windows, y que este no sea el mismo que la instalación del entorno de desarrollo. De hecho, si nos fijamos, podemos ver que algunos menús aparecen en el idioma del SO y otros en el idioma del IDE.

Para solucionarlo vamos a Tools => Options y, en Configuración Internacional / International Settings seleccionamos el idioma que nos aparece en el combo.

Por último, sería recomendable entrar en la consola de Visual Studio Tools y ejecutar el comando devenv /resetSkipPkgs

Espero que sea de utilidad.

¡Saludos!

Los webparts de Sharepoint 2007

Uno de los conceptos más básicos que debemos conocer de los portales de Sharepoint es qué es un webpart. Los elementos web nos permiten crear un contenido personalizable de una manera dinámica, donde el usuario puede interactuar con ellos ajustando sus propiedades e incluso el contenido.

A parte de los ya existentes dentro de Sharepoint, nosotros como desarrolladores podemos crear web parts a medida.

HERRAMIENTAS PARA VISUAL STUDIO

Es recomendable la instalación de las plantillas de Sharepoint para Visual Studio. Podemos descargarlas a través de los siguientes enlaces:

CREACIÓN DEL WEB PART

La forma más cómoda de trabajar con Visual Studio y Sharepoint es tener instalado el primero en el mismo sitio que el segundo. De esta manera, tendremos disponibles las librerías necesarias para trabajar con MOSS además de poder debuggear nuestras aplicaciones fácilmente.
Para este post, vamos a crear un webpart desde Visual Studio 2008 llamado BasicWebPart para MOSS 2007:

Al instalar las herramientas, tenemos una nueva rama dentro de los tipos de proyectos llamada Sharepoint. En esta ocasión vamos a seleccionar Elemento web/Web part.

A partir de este momento, tendremos un proyecto con una serie de archivos:

  • El code behind del elemento web, en el cual incluiremos las propiedades que el usuario puede configurar, cargaremos los controles para la interfaz de usuario, etcétera.
  • Un archivo con extensión .webpart donde está incluida la metadata.
  • El archivo XML donde se especifica el namespace del webpart creado.

Como vemos, no tenemos ningún archivo de tipo web que nos ayude a diseñar la interfaz de usuario. En el code behind aparece el método CreateChildControls listo para sobrescribirse pero, si tenemos una serie de controles, botones, algo de lógica, etcétera esto podría convertirse en algo realmente tedioso. Por ello, voy a crear un nuevo proyecto del tipo Web Application donde, a su vez, añadiré un user control con el siguiente código:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="TwitterClient.ascx.cs"
    Inherits="BasicUserControl.TwitterClient" %>
<div id="content">
    <div>
        <h2>"What are you doing?"</h2>
    </div>
    <div>
        <asp:TextBox ID="txtStatus" runat="server" TextMode="MultiLine" Rows="6" Columns="30"></asp:TextBox>
    </div>
    <div>
        <asp:Button ID="btnTweet" runat="server" Text="Tweet" OnClick="BtnTweetClick" />
    </div>
</div>

Y como code behind lo siguiente:


using System;
using Twitterizer.Framework;

namespace BasicUserControl
{
    public partial class TwitterClient : System.Web.UI.UserControl
    {
        public string UserName { get; set; }
        public string Password { get; set; }

        protected void Page_Load(object sender, EventArgs e) { }

        protected void BtnTweetClick(object sender, EventArgs e)
        {
            var twitterUser = new Twitter(UserName, Password);
            twitterUser.Status.Update(txtStatus.Text);
        }
    }
}

Este web part tiene como única tarea actualizar nuestro estado en Twitter. Soy consciente de que la lógica debería ser algo más elaborada (comprobar que se realizó el envío o bien controlar cualquier tipo de error) pero el objetivo de este post es simplificar los pasos para la creación de web parts lo más posible.

Una vez que tenemos el control de usuario y su lógica correspondiente, volvemos al proyecto del elemento web y asociamos el mismo al user control:


using System;
using System.Runtime.InteropServices;
using System.Web.UI;
using System.Web.UI.WebControls.WebParts;
using BasicUserControl;
using Microsoft.SharePoint.Utilities;

namespace BasicWebPart.BasicWebPart
{
    [Guid("8a91e4e4-033f-4e87-b9ea-5c7be1dec2fb")]
    public class BasicWebPart : WebPart
    {
        private TwitterClient _ucTwitterClient;
        public Exception Error { get; set; }

        [Personalizable(PersonalizationScope.Shared),
        WebBrowsable(true),
        WebDisplayName("User name"),
        WebDescription(@"Twitter user name")]
        public string UserName { get; set; }

        [Personalizable(PersonalizationScope.Shared),
        WebBrowsable(true),
        WebDisplayName("Password"),
        WebDescription(@"Twitter password")]
        public string Password { get; set; }

        protected override void CreateChildControls()
        {
            try
            {
                base.CreateChildControls();
                Controls.Clear();

                _ucTwitterClient = (TwitterClient)Page.LoadControl("_controls/Twitter/TwitterClient.ascx");
                _ucTwitterClient.UserName = UserName;
                _ucTwitterClient.Password = Password;

                Controls.Add(_ucTwitterClient);
            }
            catch (Exception ex)
            {
                Error = ex;
            }
        }

        protected override void Render(HtmlTextWriter writer)
        {
            if (Error != null)
                SPUtility.TransferToErrorPage(Error.Message + " ---> " + Error.StackTrace);
            base.Render(writer);
        }
    }
}

En primer lugar he creado una variable privada del mismo tipo que el user control que hemos creado, además de una propiedad que recogerá cualquier excepción que pudiera suceder dentro de nuestro elemento.
En el control de usuario teníamos declaradas dos propiedades para almacenar el nombre de usuario y la contraseña para la conexión con Twitter. En este caso queremos que estos datos los facilite el usuario para poder actualizar el estado de su cuenta. Por ello, se han creado dos propiedades, UserName y Password, las cuales están decoradas con una serie de atributos propios de Sharepoint.

Por último, hemos modificado el método CreateChildControls para cargar el control de usuario y poder pasarle los valores de usuario y contraseña y, además, en el método Render comprobamos si ocurrió algún error para mostrarlo o bien renderizamos el contenido.

Si compiláramos la solución, ocurriría lo siguiente:

El error nos indica que es necesario un nombre seguro para el ensamblado. Para solucionarlo, debemos crear una firma accediendo a las propiedades del proyecto:

En mi caso cree una firma sin contraseña.

PUBLICACIÓN DEL WEB PART

En el supuesto más sencillo, donde no tenemos una granja de servidores y únicamente es necesario desplegar nuestro web part en un único sitio, podríamos seguir los siguientes pasos para poder utilizar el web part que acabamos de crear en un portal de MOSS:

  1. Copiar el assembly del web part en la GAC (Global Assembly Cache). En este caso, iríamos a la carpeta bin del proyecto BasicWebPartcopiaríamos en la carpeta assembly de WINDOWS la dll  BasicWebPart.dll ubicada dentro de Debug.
  2. Accedemos a C:InetpubwwwrootwssVirtualDirectories y entramos en la carpeta del portal donde queremos utilizar el web part. Por defecto, estas carpetas tienen como nombre el número del puerto asociado al portal.
  3. Creamos un nuevo directorio llamado _controls y, dentro de él, otro llamado Twitter. Copiamos dentro el archivo ascx que creamos en el proyecto web (TwitterClient.ascx).
  4. Hacemos una copia del contenido de binDebug de nuestro proyecto BasicWebPart en la carpeta bin del directorio virtual.
  5. Modificamos el web.config del sitio y añadimos una nueva línea dentro de la sección SafeControls:
    <SafeControl Assembly="BasicWebPart, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9f4da00116c38ec5" Namespace="BasicWebPart.BasicWebPart" TypeName="*" Safe="True" />

    La clave pública del web part (PublicKeyToken) podemos localizarla en las propiedades de la dll correspondiente dentro de la carpeta assembly de WINDOWS:

  6. Abrimos una consola de MS-DOS y ejecutamos iisreset /noforce para que los cambios sean tomados por el sitio de Sharepoint.
  7. Accedemos a la configuración del sitio:
  8. En el apartado Galerías seleccionamos Elementos web.
  9. Pulsamos sobre el botón Nuevo y seleccionamos el web part dentro de la lista. Una vez localizado, hacemos clic en Llenar galería.
  10. Para facilitar la búsqueda del web part dentro del listado de los elementos disponibles, podemos modificar algunas de sus propiedades pulsando en el botón editar.
  11. Accedemos a la página donde queremos poner el nuevo elemento y pulsamos sobre Editar página. Al pulsar en cualquiera de los sitios disponibles para un nuevo elemento web, nos aparecerá un listado con los web part disponibles. Si hemos modificado las propiedades del mismo, aparecerá en el apartado Twitter.
  12. Una vez que hemos agregado el web part, pulsamos sobre el botón editar y modificamos las propiedades. En este caso, al no categorizar las que añadimos aparecerán en el apartado Varios.

Nota: Para este caso en concreto, donde estamos haciendo una llamada a través de Internet para conectar con Twitter, es necesario modificar la confianza del sitio. Cuando creas un portal, por defecto, está definida a Minimal Trust. Para modificar este parámetro, en el web.config cambiamos esta línea:

<trust level="WSS_Minimal" originUrl="" />

Por esta otra:

<trust level="Full" originUrl="" />

Más adelante hablaremos sobre ello :)

¡Saludos!