jueves, 13 de diciembre de 2018

Calcular la quincena actual en SQL

Aquí dejo un pequeño script para calcular el primer día de la quincena actual.

Básicamente, hace lo siguiente:

  • Elimina la parte de hora de la fecha actual. No es que influya en los cálculos, pero así el resultado sera una fecha con 0 horas y 0 minutos.
  • En función del día del mes, calcula el día de inicio de la quincena. 1 para fechas anteriores al 16, 15 para posteriores al 15.
  • A la fecha actual le suma el día que necesitamos (1 o 15), menos el día actual del mes.


declare @fechaActual datetime = convert(date, getdate())
declare @fechaQuincenaActual datetime
declare @dias int

set @dias = case when day(@fechaActual) < 16 then 1 else 15 end
set @fechaQuincenaActual = dateadd(day, @dias-day(@fechaActual), @fechaActual)

select @fechaQuincenaActual


Aunque sea para SQL Server (Transact-SQL), creo que sería fácilmente adaptable a cualquier otro sabor de SQL


miércoles, 21 de noviembre de 2018

Cómo excluir carpeta Packages de TFS


Es una buena práctica, cuando estamos desarrollando proyectos en Visual Studio y utilizamos paquetes NuGet pra las librerías externas, excluir del control de código fuente la carpeta Packages.

Vamos a ver muy por encima cómo funciona Visual Studio a la hora de trabajar con paquetes, y qué tenemos que tener en cuenta a la hora de trabajar con TFS como control de código fuente cuando usamos NuGet.

Visual Studio y los paquetes NuGet


Cuando usamos paquetes NuGet, los paquetes incluidos en cada proyecto se localizan en el fichero nuget.config. Este fichero es un XML en el que se definen los paquetes utilizados, así como la versión específica de cada uno de ellos que se está utilizando.

Un ejemplo de fichero nuget.config:


<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="GemBox.Spreadsheet" version="41.3.30.1111" targetFramework="net462" />
  <package id="NLog" version="4.4.12" targetFramework="net462" />
  <package id="NLog.Web" version="4.5.0" targetFramework="net462" />
</packages>



En este caso el proyecto usa tres paquetes NuGet: GemBox.Spreadsheet, NLog y Nlog.Web.

Cuando Visual Studio compila el proyecto, si no se hubieran descargado previamente los paquetes, pueden pasar dos cosas:
  • Si tenemos activada la restauración automática de paquetes, Visual Studio descargará todos los paquetes especificados en la carpeta Packages que se encuentra junto a la solución (fichero .sln)
  • Si no tenemos activada esta restauración, nos indicará que no se puede compilar porque falta descargar algunos paquetes.

Si todos los paquetes estaban ya descargados, Visual Studio simplemente compilará la solución.

Lo importante a tener en cuenta es que Visual Studio, para poder compilar, descargará los paquetes en la carpeta Packages. En el caso que nos ocupa, estos paquetes ocupan 25 archivos en 17 carpetas, y ocupan 22,8 MB.

TFS con NuGet


Supongamos que hemos montado una solución de Visual Studio que contiene varios proyectos, y que estos proyectos usan distintos paquetes NuGet.

Al compilar, se crea una carpeta Packages, junto al fichero .sln de la solución, y, como hemos visto, en esta carpeta Visual Studio descarga todos los paquetes (ficheros .dll, ficheros de script para la instalación / desinstalación del paquete, ficheros .js, etc.)

Ahora añadimos nuestra solución a nuestro control de código fuente en TFS, porque tenemos que compartirla con el resto del equipo de desarrollo. Lo que ocurrirá es que TFS añadirá toda la carpeta Packages al control de código fuente, y la subirá al servidor.

Este comportamiento por defecto puede acarrear algunos problemas a futuro:
  • Estamos almacenando en nuestro servidor TFS código que no es nuestro (son librerías pre compiladas externas), y que pueden ocupar bastante espacio. No es difícil que un proyecto use diez o veinte paquetes NuGet, sobre todo en versiones modernas del framework .Net de Microsoft
  • Al actualizar versiones de paquetes, algo que podremos hacer si aparecen nuevas versiones y nos interesa usarlas porque añaden o corrigen alguna funcionalidad, este espacio de almacenamiento se multiplica, porque TFS guardará cada versión en el servidor.
  • Las operaciones de descarga de la última versión, check-in, check-out, branch y merge serán más lentas, porque afectarán a más archivos.
  • En el caso de que configuremos TFS de forma que no permita hacer check-out simultaneo del mismo fichero por dos archivos, a veces los desarrolladores se bloquearán unos a otros sin quererlo, al actualizar un paquete o simplemente porque alteran algo sin darse cuenta.

La cuestión es que, como hemos visto, Visual Studio no necesita los paquetes en sí mismos para poder compilar la solución. Lo que necesita es la definición de los paquetes que se utilizan (el fichero packages.config de cada proyecto). Vamos a ver cómo evitar que se suban los paquetes descargados a su servidor, manteniendo la capacidad de compilar y descargar automáticamente paquetes.

Configurar Visual Studio y NuGet


Para conseguir el comportamiento que deseamos, tenemos que modificar dos ficheros que se encontrarán junto a la solución (fichero .sln). Si los ficheros no existen tendremos que crearlos.

El primer fichero que hay que modificar es el fichero .tfignore. Este fichero almacena las rutas de carpetas y archivos que queremos excluir del control de código fuente. Hay que añadir las siguientes líneas:


packages
!\packages\repositories.config


Esto significa que se excluirá todo lo que haya en la carpeta paclages, y que no se excluirá (sí que se subirá a TFS) el fichero repositories.config. La exclamación delante de la línea está negando la exclusión.

Parece que con esto debería ser suficiente, pero resulta que no. El gestor de paquetes de NuGet mantiene su propia integración con TFS, y tenemos que indicarle que no realice esta integración. Para ellos añadiremos (o editaremos) el fichero NuGet.config. En este fichero introduciremos lo siguiente:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <solution>
    <add key="disableSourceControlIntegration" value="true" />
  </solution>
</configuration>


Si el fichero ya existía deberemos revisarlo para integrar este código.

Esto lo que hace es indicar a NuGet que no se integre con TFS, de forma que los paquetes referenciados no se almacenarán en el servidor.

De esta forma ahorraremos espacio, ganaremos tiempo en algunas operaciones de TFS y evitaremos conflictos en edición simultanea de paquetes entre compañeros del equipo.

miércoles, 21 de enero de 2015

Aceptación explícita de cookies Google Analytics / Universal Analytics para cumplir LSSI

La LSSI, y más concretamente su desarrollo en el RD 13/2012 de 30 de marzo, indica que el uso de cookies (y otros dispositivos de almacenamiento, como local storage de HTML5, o los etags) debe ser consentido por el usuario, tras haber sido adecuadamente informado de que elementos de alamacenamiento se utilizan y fines de los mismos.

Los prestadores de servicios podrán utilizar dispositivos de almacenamiento y recuperación de datos en equipos terminales de los destinatarios, a condición de que los mismos hayan dado su consentimiento después de que se les haya facilitado información clara y completa sobre su utilización, en particular, sobre los fines del tratamiento de los datos, con arreglo a lo dispuesto en la Ley Orgánica 15/1999, de 13 de diciembre, de Protección de Datos de Carácter Personal.

Cuando sea técnicamente posible y eficaz, el consentimiento del destinatario para aceptar el tratamiento de los datos podrá facilitarse mediante el uso de los parámetros adecuados del navegador o de otras aplicaciones, siempre que aquél deba proceder a su configuración durante su instalación o actualización mediante una acción expresa a tal efecto.

Lo anterior no impedirá el posible almacenamiento o acceso de índole técnica al solo fin de efectuar la transmisión de una comunicación por una red de comunicaciones electrónicas o, en la medida que resulte estrictamente necesario, para la prestación de un servicio de la sociedad de la información expresamente solicitado por el destinatario.

Sin lugar a duda habréis visto muchas webs en las que se indica al usuario que se usan cookies y que si continúa navegando acepta el uso de las mismas. Esto es correcto, siempre y cuando se implemente adecuadamente. El error fundamental detrás de este mensaje es que, en gran cantidad de ocasiones, se fijan las cookies ANTES de que el usuario acepte o navegue.

Aquí dejo un script que he utilizado en algún sitio para realizar la carga de Analytics DESPUES de que el usuario haya aceptado.


// Identificación de la cuenta y sitio en Google Analytics
var gaAccountId = "Aqui el ID de Google Analytics";
var gaSiteName = "Aqui el dominio";
// Si se desea depurar en localhost, para que funcione analytics se debe usar el dominio 'none'
// var gaSiteName = { 'cookieDomain': 'none' };
// Nombre de la cookie en la que se guardan las preferencias del usuario respecto a cookies de terceros
var gaOptInCookieName = "gaOptIn";
// Selectores jQuery de:
// - Contenedor de la pregunta de cookies, para mostrarlo u ocultarlo según proceda. 
// - Elemento en el que el usuario hará clic para aceptar cookies
// - Elemento en el que el usuario hará clic para rechazar cookies
var gaPreferenceQuestionSelector = ".gaOptIn";
var gaOptInButtonSelector = ".gaOptInBtn";
var gaOptOutButtonSelector = ".gaOptOutBtn";

// Funciones para evaluar anteriores respuestas del usuario.
function gaHasNotExpressedPreference() {
    return ($.cookie(gaOptInCookieName) == undefined);
}

function gaHasOptedOut() {
    if (!gaHasNotExpressedPreference()) {
        return ($.cookie(gaOptInCookieName) == "No");
    }
    return false;
}

function gaHasOptedIn() {
    if (!gaHasNotExpressedPreference()) {
        return ($.cookie(gaOptInCookieName) == "Yes");
    }
    return false;
}

// Rechazo del uso de cookies
function gaOptOut() {
    window["ga-disable-" + gaAccountId] = true;
    $.cookie(gaOptInCookieName, 'No', { expires: 720, path: '/' });
}

// Aceptación del uso de cookies
function gaOptIn() {
    window["ga-disable-" + gaAccountId] = false;
    $.cookie(gaOptInCookieName, 'Yes', { expires: 720, path: '/' });
    gaTrackPageView();
}

// Muestra la pregunta sobre el uso de cookies
function gaShowPreferenceQuestion() {
    var gaPreferenceQuestion = $(gaPreferenceQuestionSelector);
    if (gaPreferenceQuestion.length > 0) {
        gaPreferenceQuestion.slideDown("slow");
    }
}

// Oculta la pregunta del uso de cookies.
function gaHidePreferenceQuestion(slide) {
    var gaPreferenceQuestion = $(gaPreferenceQuestionSelector);
    if (gaPreferenceQuestion.length > 0) {
        if (slide) {
            gaPreferenceQuestion.slideUp("fast", function () {
                gaPreferenceQuestion.remove();
            });
        }
        else {
            gaPreferenceQuestion.remove();
        }
    }
}

// Realiza la inserción del script de analytics.
// Además, lanza el tracking de la página, para no perder el hit por no estar cargado el script de analytics.
function gaTrackPageView() {
    (function (i, s, o, g, r, a, m) {
        i['GoogleAnalyticsObject'] = r;
        i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments) }, i[r].l = 1 * new Date();
        a = s.createElement(o), m = s.getElementsByTagName(o)[0];
        a.async = 1;
        a.src = g;
        m.parentNode.insertBefore(a, m)
    })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');

    ga("create", gaAccountId, gaSiteName);
    ga("send", "pageview");
}

// Script de ejecución en carga de página. 
// Asocia los eventos a los elementos de aceptación y rechazo, 
// Evalua la cookie donde se almacenaron las preferencias del usuario para, en función de respuestas
// anteriores, lanzar o no el tracking de página y para mostra u ocultar el mensaje sobre cookies.
$(document).ready(function () {
    $(gaOptInButtonSelector).click(function () {
        gaOptIn();
        gaHidePreferenceQuestion(true);
    });

    $(gaOptOutButtonSelector).click(function () {
        gaOptOut();
        gaHidePreferenceQuestion(true);
    });

    if (gaHasNotExpressedPreference()) {
        window["ga-disable-" + gaAccountId] = true;
        gaShowPreferenceQuestion();
    }
    else if (gaHasOptedOut()) {
        gaHidePreferenceQuestion(false);
    }
    else {
        window["ga-disable-" + gaAccountId] = false;
        gaHidePreferenceQuestion(false);
        gaTrackPageView();
    }
});


Como podréis apreciar el script usa jQuery para realizar las operaciones con el DOM, así que si no podéis usar jQuery, tendréis que reescribir ciertas partes para adaptarlas a JavaScript estándar o a la librería JavaScript que utilicéis.

Por otro lado, y antes de que alguien pregunte (si alguien pasa alguna vez por aquí), se está fijando una cookie para registrar la preferencia del usuario, aunque diga que no quiere cookies. ¿Cómo es posible? Según dice que no, le fijamos una cookie. ¡Toma ya! Pues es correcto, porque esta cookie es lo que se considera una cookie de carácter técnico. La normativa permite el uso de cookies que sean necesarias para prestar un servicio que ha solicitado expresamente el usuario. Dentro de este paraguas estaría la cookie para guardar su preferencia, así como también la cookie de ASP.Net o de forms authentication para mantener la sesión del usuario.

lunes, 16 de septiembre de 2013

Controlar contraseñas repetidas en SQLMembershipProvider

A veces las aplicaciones deben cumplir una serie de requisitos de seguridad en lo que se refiere a la autentificación de usuarios. En muchas ocasiones los providers de membership .Net proporcionan suficientes características para cumplir con estos requisitos, como seguridad de contraseña, encriptación de la misma, bloqueo tras reintentos, etc.

En otras ocasiones esto no resulta ser así, como es el caso que nos ocupa. Se trata de evitar que el usuario repita una contraseña que ya ha sido utilizada previamente. En concreto, se trata de evitar que el usuario reutilice cualquiera de las últimas diez contraseñas.

Este requisito apareció a posteriori sobre un desarrollo que ya utilizaba el provider de membership SQLMembershipProvider.

A la hora de implementar esta nueva funcionalidad, podemos optar por dos caminos:

  • Opcion 1: Implementar un provider de Membership que, a la hora de evaluar si una contraseña es válida para su uso, a las comprobaciones ya existentes de longitud, número de simbolos, etc., añada la comprobación de contraseñas repetidas.
  • Opción 2: Intentar realizar esta comprobación usando el evento ValidatingPassword de membership, que permite añadir comprobaciones adicionales a medida, que se sumarán a las ya realizadas por defecto por el provider utilizado.


En nuestro caso vamos a optar por la opción 2. Usar el evento ValidatingPassword, junto con algunas modificaciones en la BD y algo de código adicional para dar soporte al almacén de contraseñas antiguas. A grandes rasgos, vamos a:

  1. Añadir una tabla "aspnet_PassHistory", en la que almacenaremos las contraseñas a medida que se van cambiando.
  2. Crear dos triggers sobre la tabla "aspnet_Membership", para "capturar" el cambio o creación de contraseñas, y poder así guardar una copia de las contraseñas.
  3. Un procedimiento almacenado "aspnet_Membership_GetOldPasswords" para obtener los datos de las contraseñas antiguas, para poder así detectar si la nueva contraseña ya fue utilizada.
  4. Un poco de "reflection" para poder acceder a ciertos métodos del provider que están calificados como "Internal".

Añadir tabla para almacenar contraseñas

Con este script se crea la tabla para almacenar contraseñas:

create table aspnet_PassHistory
(
    ApplicationId uniqueidentifier not null,
    UserId uniqueidentifier not null,
    Password nvarchar(128) not null,
    PasswordFormat int not null,
    PasswordSalt nvarchar(128) not null,
    ChangedOn datetime not null,
    constraint PK_aspnet_PassHistory primary key (UserId, ChangedOn)
)

Fijaos en que para cada usuario se almacenarán una colección de contraseñas, pero también el formato en que se codifica (sin codificar, hash, encriptación, etc.), así como el "salt" utilizado para reforzar la seguridad de la encriptación.
Además, con el objeto de poder eliminar contraseñas que ya no interesa mantener como "prohibidas", guardamos la fecha en la que se fijó cada contraseña.

Triggers para alimentar la tabla de histórico de contraseñas

Se utilizan dos triggers sobre la tabla "aspnet_Membership". El primero de ellos se encarga de almacenar las contraseñas en la tabla "aspnet_PassHistory" cada vez que una nueva contraseña se inserta / modifica. El segundo se encarga de eliminar las contraseñas guardadas cuando se elimina un usuario de membership.

create trigger aspnet_Membership_PassChange
on aspnet_Membership
for insert, update
as
begin
    if update(Password)
    begin
        insert into aspnet_PassHistory (ApplicationId, UserId, Password, PasswordFormat, PasswordSalt, ChangedOn)
        select ApplicationId, UserId, Password, PasswordFormat, PasswordSalt, GetDate() from inserted
    end
end

create trigger aspnet_Membership_DeleteUser
on aspnet_Membership
for delete
as
begin
    delete from aspnet_PassHistory where UserId in (select UserId from deleted)
end


Este segundo trigger podría sustituirse por una referencia de integridad entre las dos tablas y un borrado en cascada.

Procedimiento almacenado para obtener las últimas "n" contraseñas

El siguiente procedimiento almacenado devuelve, para una aplicación y usuario específico, la relación de contraseñas usadas hasta el momento. Además, dado que recibe un parámetro que determina el número de contraseñas a evaluar, se encarga de hacer limpieza en la tabla, eliminando las contraseñas guardadas que ya no son útiles.

create procedure aspnet_Membership_GetOldPasswords
    @ApplicationName    nvarchar(256),
    @UserId             uniqueidentifier,
    @NumSavedPasswords  int
as
begin
    -- Eliminamos las filas más antiguas del usuario, en función del número de contraseñas que se deben guardar
    ;with RowsToKeep as
    (
        select top(@NumSavedPasswords) aspnet_PassHistory.ApplicationId, aspnet_PassHistory.UserId, aspnet_PassHistory.ChangedOn
        from aspnet_Applications inner join aspnet_PassHistory on aspnet_Applications.ApplicationId = aspnet_PassHistory.ApplicationId
        where aspnet_Applications.LoweredApplicationName = Lower(@ApplicationName) and aspnet_PassHistory.UserId = @UserId
        order by aspnet_PassHistory.ChangedOn desc
    )
    delete aspnet_PassHistory 
    from aspnet_PassHistory left join RowsToKeep 
    on aspnet_PassHistory.ApplicationId = RowsToKeep.ApplicationId and aspnet_PassHistory.UserId = RowsToKeep.UserId and aspnet_PassHistory.ChangedOn = RowsToKeep.ChangedOn
    where RowsToKeep.ChangedOn is null
    
    -- Seleccionamos las contraseñas guardadas restantes
    select aspnet_PassHistory.ApplicationId, 
    aspnet_PassHistory.UserId, 
    aspnet_PassHistory.Password, 
    aspnet_PassHistory.PasswordFormat, 
    aspnet_PassHistory.PasswordSalt,
    aspnet_PassHistory.ChangedOn
    from aspnet_Applications inner join aspnet_PassHistory on aspnet_Applications.ApplicationId = aspnet_PassHistory.ApplicationId
    where aspnet_Applications.LoweredApplicationName = Lower(@ApplicationName) and aspnet_PassHistory.UserId = @UserId
end


Acceder a la tabla de passwords antiguas a través de Entity Framework

Para acceder a la tabla de passwords antiguas se puede usar cualquier tecnología de BD que se considere adecuada, pero en nuestro caso, por ser consistentes con el resto del desarrollo, vamos a hacerlo con Entity Framework, incluyendo elementos en un modelo "Database First", que se llama ComunModel, y que ya tenemos en el proyecto para hacer varias tareas. No voy a entrar en detalles de como se hace cada paso, pero básicamente, hay que hacer lo siguiente:

  1. Añadir en el modelo la tabla "aspnet_PassHistory" y el procedimiento almacenado "aspnet_Membership_GetOldPasswords"
  2. Renombrar la entidad y el "EntitySet" creado para "aspnet_PassHistory" como "OldPassword" y "OldPasswords" respectivamente.
  3. Añadir una "Import Function" en el modelo, para poder ejecutar el procedimiento almacenado "aspnet_Membership_GetOldPasswords". A esta nueva "Import Function" la hemos llamado "GetOldPasswords"

Evento ValidatingPassword

El evento Membership.ValidatingPassword se dispara cuando se va a fijar una nueva contraseña. Esto puede ocurrir al crear un nuevo usuario, al cambiar de contraseña, o al resetearla(fijar una nueva contraseña sin conocer la antigua). Vamos a incluir en global.asax.cs el código para hacer esta comprobación.

Primero definimos el manejador del evento, en Application_Start:

protected void Application_Start(object sender, EventArgs e)
{
    Membership.ValidatingPassword += new MembershipValidatePasswordEventHandler(Membership_ValidatingPassword);
}


Y luego lo implementamos:

void Membership_ValidatingPassword(object sender, ValidatePasswordEventArgs e)
{
    e.Cancel = false;
    if (!e.IsNewUser)
    {
        List<OldPassword> passwords = GetOldPasswords();

        foreach (OldPassword oldPwd in passwords)
        {
            string newEncodedPwd = EncodePassword(e.Password, oldPwd.PasswordFormat, oldPwd.PasswordSalt);
            if (newEncodedPwd == oldPwd.Password)
            {
                e.Cancel = true;
                e.FailureInformation = new Exception("La contraseña ya ha sido utilizada previamente. Por motivos e seguridad no se pueden repetir.");
                break;
            }
        }
    }
}

private string EncodePassword(string password, int passwordFormat, string passwordSalt)
{
    MethodInfo dynMethod = Membership.Provider.GetType().GetMethod("EncodePassword", BindingFlags.NonPublic | BindingFlags.Instance);
    object methodResult = dynMethod.Invoke(Membership.Provider, new object[] { password, passwordFormat, passwordSalt });
    return methodResult as string;
}

List<OldPassword> GetOldPasswords()
{
    List<OldPassword> passwords = new List<OldPassword>();
    MembershipUser usuarioActual = Membership.GetUser(false);
    if (usuarioActual != null)
    {
        using (ComunModel comunModel = new ComunModel())
        {
            passwords = comunModel.GetOldPasswords(Membership.ApplicationName, new Guid(usuarioActual.ProviderUserKey.ToString()), Entorno.NumPasswordsAntiguas).ToList();
        }
    }
    return passwords;
}



Codificación de passwords. System.Reflection al rescate.


Dado que las contraseñas en la BD de membership pueden guardarse encriptadas, no podemos comparar alegremente las contraseñas guardadas con la contraseña que intentamos validar, porque si las contraseñas guardadas están almacenadas en "abierto", sin encriptar o sin hacer hash, nunca coincidirían, aun siendo iguales.

Mirando la implementación del provider SQLMembershipProvider publicada por Microsoft, y que es de dominio publico, me di cuenta de que necesitaba llamar al método EncodePassword, con los parámetros adecuados, para calcular la contraseña en el formato adecuado, y así poder compararla con la almacenada.

El problema es que el método EncodePassword está clasificado como "internal", de forma que no es accesible desde ensamblado distinto al del propio provider. Para poder llamar a este método, recurrí a la muy util reflexión, que si que nos permite obtener una referencia a un método privado, internal o protected, y ejecutarlo:


private string EncodePassword(string password, int passwordFormat, string passwordSalt)
{
    MethodInfo dynMethod = Membership.Provider.GetType().GetMethod("EncodePassword", BindingFlags.NonPublic | BindingFlags.Instance);
    object methodResult = dynMethod.Invoke(Membership.Provider, new object[] { password, passwordFormat, passwordSalt });
    return methodResult as string;
}


¿Y en otras implementaciones?

Este esquema podría ser válido para otras implementaciones de MembershipProvider, como por ejemplo, las de Oracle o MySQL, pero habría que hacer algunas tareas de investigación antes de poder afirmarlo categóricamente. En mi opinión habría que:

  1. Crear las tablas scripts adecuados para el sistema de base de datos utilizado.
  2. Analizar, bien usando el código fuente, bien haciendo ingeniería inversa con, por ejemplo, ILSpy, el provider que queremos extender, para estudiar la forma en que se codifica la contraseña. Puede que incluso hayan hecho el método de codificar contraseñas público.

miércoles, 6 de marzo de 2013

Métodos extensores que no compilan en SharePoint 2010

Cuando en SharePoint 2010 intentamos usar en un user control o un visual web part un método extensor que hemos definido en un assembly .Net 3.5, nos podemos encontrar con el error:
CS0117: 'TipoDeDatos' does not contain a definition for 'MetodoExtensor'.
Este error se produce porque SharePoint está configurado para que realice la compilación dinámica con una versión del compilador de C# que no soporta todas las características de C# 3.5.
Podemos cambiar esto modificando la forma en que nuestra aplicación compila dinámicamente los ficheros .ascx, incluyendo esto en el web.config:
<system.codedom="">
   <compilers>
      <compiler extension=".cs" language="c#;cs;csharp" type="Microsoft.CSharp.CSharpCodeProvider, System, 
               Version=2.0.0.0, Culture=neutral, 
               PublicKeyToken=b77a5c561934e089" warninglevel="4">
         <provideroption name="CompilerVersion" value="v3.5"/>
         <provideroption name="WarnAsError" value="false"/>
      </compiler>
   </compilers>
</system.codedom> 


viernes, 1 de marzo de 2013

TFS 2012 gratis

Se acabó andar guardando nuestros proyectos personales y pruebas en mil sitios: en casa, en el trabajo, en una memoria flash que a saber donde andará...

Microsoft ofrece TFS 2012 gratis, con ciertas limitaciones. Podéis consultar las condiciones y limitaciones en http://tfs.visualstudio.com.

Ojo, que no sólo se trata de control de código fuente, si no de todas las funcionalidades de TFS. Ayer, cuando creé un proyecto nuevo, me permitió elegir entre tres metodologías (dos ágiles y una CMMI).

La versión gratuita del servicio permite hasta cinco cuentas, con lo que es muy útil para los proyectos que realices con tus compañeros o amigos fuera del entorno empresarial.

Además, de momento, se puede utilizar sin coste adicional la integración continua y la ejecución automatizada de proyectos de test. Estas dos funcionalidades parece que serán de pago, pero de momento, mientras las afinan, se pueden usar.

Los que como yo uséis VS2010, puede que tengáis que instalar un parche para la compatibilidad VS2010 - TFS 2012. La información sobre este parche la podéis encontrar en http://www.microsoft.com/en-us/download/details.aspx?id=29082

A disfrutarlo.

lunes, 25 de junio de 2012

¿Qué hacer si un control adapter parece no funcionar?

En ASP .Net, los "control adapters" son controles que permiten modificar la forma en la que otro control se presenta, para adaptar el html generado a nuestras necesidades. Un ejemplo de estos controles podrían ser los CSS friendly adapters.

Un control adapter siempre está contenido en una DLL (puede ser la misma que genera nuestra aplicación web) que se debe desplegar en el directorio Bin de la aplicación. El paso final para el registro de un control adapter se hace a través de un fichero .browser, dentro de la carpeta app_browsers de la aplicación ASP .Net. Podeis mirar como se hace un control adapter y como se registra en muchos sitios, como en este artículo sobre control adapters.

Si queremos que la influencia de un control adapter sobrepase a nuestra aplicación web, digamos a nivel de máquina, el registro de este adaptador es ligeramente distinto, así como la ubicación de la DLL.

Lo más habitual es desplegar la DLL en la GAC, para lo que esta deberá estar firmada. Una vez hemos colocado la DLL en la GAC, el registro se hace también a través de un fichero .browser, pero en este caso este fichero debe colocarse en una carpeta del framework .net. En el caso de una máquina de 32 bits, sería en la carpeta "C:\Windows\Microsoft.NET\Framework\v2.0.50727\CONFIG\Browsers". Si se trata de una máquina de 64 bits, la carpeta sería, como parece lógico, "C:\Windows\Microsoft.NET\Framework64\v2.0.50727\CONFIG\Browsers".

Una vez colocada la DLL en la GAC y el fichero browser en la correspondiente carpeta del Framework, hay que generar una dll para que el framework sea capaz de aplicar el control adapter cuando sea necesario. Para generar esta DLL se debe utilizar el comando

aspnet_regbrowsers -i

Este comando genera una DLL que se instala en la GAC y que permite que el control adapter que hemos instalado actue.

En este escenario de control adapter que se publica a nivel de máquina, a veces no se consigue el efecto esperado. El síntoma es que, aunque se siguen los pasos y no parece que haya habido errores, el control adapter no consigue entrar en acción.

El escenario real en el que nos ha pasado en nuestro trabajo es el de un control adapter que se usa para mejorar la accesibilidad de Sharepoint 2007. Este adapter (en conjunción con otros elementos) modifica el HTML generado por el control WebPartZone, de forma que, cuando estamos en una página de publicación pero no la estamos editando, este control no genera tablas.

Pues bien, aunque parece bien registrado, el control adapter no actúa y sigue presentando tablas. No importa las veces que se ejecute el comando de registro ni las veces que se haga iisreset, no funciona. ¿Por qué? Pues la verdad es que no sabría decir por qué, pero si puedo decir cómo hemos conseguido solucionarlo.

En Sharepoint, cada aplicación web o cada extensión de las mismas tiene un directorio raíz, ubicado en c:\inetpub\wwwroot\wss\virtualdirectories. Dentro de estas aplicaciones web, que Sharepoint crea automáticamente, se ubica una carpeta app_browsers, con un fichero compat.browsers. Pues resulta que abriendo esos ficheros, modificándo cualquier parte (un espacio ente dos tags, por ejemplo, que no altera el significado del xml), y haciendo iisreset, se soluciona.

Mi hipótesis:las aplicaciones se crearon antes de que se registrara el control adapter a nivel de máquina, y, de algún modo, sus compat.browsers se compilaron y almacenaron en alguna misteriosa ubicación. Una vez introducido el nuevo .browser a nivel de máquina, no importa lo que hicieramos, estas versiones compiladas y misteriosamente almacenadas no se actualizaban con la nueva información. Al hacer la modificación en los ficheros compat.browser a nivel de aplicación, se recompilaron, teniendo en cuenta esta vez las configuraciones a nivel de máquina.

Esto nos habría venido muy bien saberlo hace un par de años, porque en más de una ocasión hemos perdido bastantes horas en algún cliente intentando implantar estos adaptadores, hasta que milagrosamente empezaron a funcionar. Parece que no tan milagrosamente, después de todo...

viernes, 30 de diciembre de 2011

Acceder a las propiedades de una columna de un modelo de Entity Framework con Visual Studio 2010 si sólo usas teclado

¿Qué pasa si eres ciego y quieres o necesitas utilizar un ordenador?

La mayoría de los usarios de informática no pensamos en lo que seríamos capaces de hacer si nos quitaran la pantalla. Y cuando nos quitan la pantalla, automáticamente podemos asumir que nos han quitado el ratón, porque no vemos el puntero.

Pues las personas ciegas se encuentran en esa situación, y para poder utilizar un ordenador recurren a lo que se denominan ayudas técnicas. En concreto, las ayudas técnicas para que los usuarios ciegos puedan utilizar el ordenador son los lectores de pantalla. El usuario se mueve por los distintos elementos de la pantalla (ventanas, botones, textos, listas, enlaces...) utilizando el teclado, y el lector de pantalla se encarga de ir "cantando" qué es cada elemento y el texto, que acción produce, etc.

Hay varios lectores de pantalla, pero en España la gran mayoría de los usuarios de Windows utilizan  JAWS, de la empresa Freedom Scientific. Otro lector de pantalla utilizado es un proyecto open source, NVDA. Normalmente, al menos en el entorno de mis compañeros de trabajo, se utiliza JAWS, y cuando se tienen dificultades para acceder a algún elemento utilizando este lector, se cambia a NVDA y se intenta realizar la tarea, pero no siempre se consigue. Además, el cambio de un lector a otro es como poco bastante molesto, porque mientras se cambia de lector no se dispone de soporte de voz.

Uno de mis compañeros trabaja habitualmente con Visual Studio 2010, y a veces se encuentra con elementos del IDE a los que no se llega fácilmente con teclado y JAWS. Cuando pasa esto, para evitar el cambio a NVDA,  intentamos buscar una forma alternativa para que llegue desde teclado a lo que nosotros hacemos con ratón de forma intuitiva.

En este caso, el problema es para acceder a las propiedades de una propiedad en una entidad de un modelo de Entity Framework. Los que usamos la pantalla y el ratón, lo haríamos simplemente abriendo el modelo de entidades en el diseñador, y haríamos clic en la propiedad que deseamos modificar. Al hacerlo, automáticamente se nos mostraría en la ventana de propiedades, si la tenemos visible. Si no la tenemos visible (la hemos cerrado o está configurada como auto-ocultar, o está organizada con otras ventans de forma que no se muestra automáticamente), con pulsar F4 se mostraría. También lo podríamos hacer con un clic del botón derecho del ratón, y desde el menú contextual podríamos acceder a ellas.

Si se abre el modelo de entidades y se intenta navegar con él con tab y teclado no se puede llegar directamente a las propiedades de una entidad, porque al usar las teclas, lo que pasa es que se mueven las entidades en el diseñador, pero no se llega a las propiedades.

Para poder acceder al modelo de entidades sin necesidad de usar el diseñador, se debe utilizar el "Entity Data Model Browser". Para abrirlo, se hace a través del menú principal de Visual Studio: View -> Other windows -> Entity Data Model Browser. En esta ventana se puede navegar por un árbol que contiene todas las entidades y sus propiedades, así como el esquema de la base de datos y las propiedades de las tablas y columnas. Si se quiere acceder directamente a la entidad desde el diseñador, una vez que tenemos seleccionada la entidad, se saca el menú contextual, y desde ahí se puede acceder al Model Browser, ya posicionados en la entidad.

lunes, 21 de noviembre de 2011

Entity Framework 4 con SQL Server 2000

A dia de hoy aún quedan sistemas basados en SQL Server 2000, para los que una migración a versiones 2005 o 2008 no es posible. Bien porque no se dispone de hardware y/o software necesario, o porque hay funcionalidad basada en paquetes DTS que no se puede migrar debido al coste de desarrollo que implica.

En mi tabajo tenemos una base de datos para controlar las horas de dedicación del personal a los distintos proyectos, que en el entorno de desarrollo se tiene en un SQL Server 2005, pero en el servidor de producción está aún en 2000.

Cuando se planteó realizar cienta nueva funcionalidad sobre esta base de datos, ni cortos ni perezosos nos metimos a hacerlo con VS 2010 y EF 4. En el entorno de desarrollo todo fue miel sobre hojuelas, pero cuando lo pasamos a producción nos encontramos con este hermoso error:

"An error occurred while executing the command definition. See the inner exception for details."

Mirando la excepción original, y usando intellitrace, nos encontramos con que EF 4 genera código SQL que no es válido para SQL Server 2000. En concreto, esta sentencia de LINQ:


Usuario primer = (from Usuario u in ctx.Usuarios 
                  orderby u.NomUsu 
                  select u).FirstOrDefault();


Genera la siguiente sentencia SQL:

SELECT TOP (1) 
[Extent1].[IDUsu] AS [IDUsu], 
[Extent1].[NomUsu] AS [NomUsu], 
.
.
.
FROM (SELECT 
[Usuario].[IDUsu] AS [IDUsu], 
.
.
.
FROM [dbo].[Usuario] AS [Usuario]) AS [Extent1]
ORDER BY [Extent1].[NomUsu] ASC


En concreto, el problema de esta sentencia es la forma en la que se ha generado el "top", ya que usa paréntesis, y en sql server 2005 si es válida, pero en sql 2000 debe realizarse sin paréntesis:


SELECT TOP 1...


Parece que lo tenemos mal, porque sin los métodos First() o FirstOrDefault() las sentencias de LINQ se complican, ya que habría que recuperar colecciones completas. Y no hablemos de las paginaciones, ya que este mismo problema se produce con el método Take().

¿Cómo se puede solucinar este problema? Pues Microsoft no da soporte de SQL Server 2000 en las herramientas de EF 4 (el diseñador, por ejemplo), pero el runtime si que tiene soporte de SQL 2000. Para hacer que nuestro modelo de entidades genere código válido para SQL 2000 tenemos que cambiar el XML del fichero edmx. Para ello, en VS 2010 hacemos clic con el botón derecho en el fichero edmx, y elegimos "Open with..." y "XML (Text) Editor". Esto abrirá el fichero como si de cualquier XML se tratara. Buscamos el elemento "edmx:Runtime", dentro de este, el "edmx:StorageModels", y dentro de este, el elemento "Schema". En este elemento, buscamos el atributo "ProviderManifestToken", y cambiamos su valor, que será 2005 o 2008 en función de la versión que usemos en desarrollo, a "2000". Quedaría algo así:


<Schema Namespace="DSTTSModel.Store" Alias="Self" 
Provider="System.Data.SqlClient" ProviderManifestToken="2000" 
xmlns:store="http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator"
xmlns="http://schemas.microsoft.com/ado/2009/02/edm/ssdl">



¡Voilá! Si compilamos y ejecutamos ya no nos aparece el error en tiempo de ejecución.

Esto no quiere decir que TODO lo que se puede hacer con LINQ + EF 4 se pueda hacer en SQL Server 2000, pero hemos salvado el primer escollo, y no uno pequeño.

A cosica hecha


Si tienes que buscar una nueva dirección de correo electrónico en gmail, o tienes que elegir un gamertag para tu cuenta XBOX Live, o tienes que elegir un título y su correspondiente dominio para un blog, estás perdido. Está todo reservado. Si haces caso a las recomendaciones que te hacen los sistemas de registro, acabas valorando muy seriamente la posiblidad de disfrutar de una dirección de correo que utilice 1337 5p34k.

Cuando me puse a buscar un nombre para el blog, me acordé del amplio espectro de palabras originarias de Murcia que utilizan mis padres, y algunas de las cuales han pasado a mi vocabulario. Y me dije, ¿habrá algo divertido para poner en el blog?

Buscando, buscando, me encontré con el Palabrero Calasparreño. Una recopilación de palabras y expresiones habituales en este pueblo de Murcia, muchas de las cuales se utilizan también en el resto de la comunidad autónoma. Recomiendo su lectura a los que, como yo, tienen familiares nacidos en esta región. En mi caso, me ha arrancado alguna que otra sonrisa, y por qué no, algún recuerdo de la infancia.

Entre todas las expresiones de este diccionario, nos encontramos "A cosica hecha", con su correspondiente definición: "Hacer algo a cosica hecha es hacerlo a propósito, con premeditación".

Y aunque sé que no actualizaré el blog con la frecuencia deseada, e incluso es posible que muera con tan solo un par de entradas en él, yo me pongo a ello "a cosica hecha".