domingo, 20 de noviembre de 2016

Versionado ASP.NET Web API 2

Hace unos meses tuvimos que crear una Web API y, por supuesto, uno de los requisitos principales era versionarla (también tenía que funcionar, pero eso no prometía ser tan divertido). Aunque inicialmente estuvimos sopesando la idea de usar algún paquete Nuget (como por ejemplo https://github.com/Sebazzz/SDammann.WebApi.Versioning) al final y en un alarde de “vamos a hacerlo a mano que así aprendemos más y no adquirimos una dependencia” nos tiramos al barro e implementamos el versionado 100% home-made. Si ha sido o no una decisión correcta lo sabremos con el tiempo…. Para más inri, han aparecido nuevos paquetes Nuget que prometen mucho http://www.hanselman.com/blog/ASPNETCoreRESTfulWebAPIVersioningMadeEasy.aspx pero ya es demasiado tarde (al menos por ahora).

Por otro lado y como hace poco me descubrí a mí mismo preguntándome qué diablos hacía este código, es ese el motivo de escribir este post, dejar por escrito que motivaciones nos empujaron en su día a tomar un montón de decisiones que parecían oportunas y llenas de razón… y aquí aprovecho para meter el disclaimer “lo hemos hecho lo mejor que hemos podido”

Viendo algunos de los distintos tipos de versionado más populares, irá discurriendo el post.

URI Path

Versionar por ruta. El más común, primigenio e intuitivo.

Aunque los ejemplos inmediatos no pueden considerarse una buena práctica (de hecho, nadie lo haría así), partiendo de la ruta “/api/Customers” lo más sencillo si queremos versionar sería crear un nuevo controlador y añadirle al nombre un sufijo de número de versión.

public class Customers2Controller : ApiController
{
    // GET api/Customers2
    public IEnumerable<Customer2> Get()
    {
    }
}

Ahora, además de “/api/Customers” también tendríamos “/api/Customers2”.

Una segunda opción más recomendable sería utilizar el atributo Route.

[Route("api/Customers2")]
public IEnumerable<Customer2> Get()
{
}

Otras rutas válidas (y seguro más convenientes que las vistas hasta ahora) serían:

  • api/v2/Customers
  • apiv2.example.com/Customers

En la primera el número de versión está en un segmento de la ruta lo más a la izquierda posible.

En la segunda es el nombre de host quien incluye el número de versión. De hecho, “dicen” que algunos incluso crean un alias para que api.example.com apunte a api<última_versión>.example.com.

En ambos casos, lo mejor para no complicarse la vida con el número de versión es que sea un simple número y no un major.minor.patch. Además, si hacemos obligatorio el uso de versión, esto es que no funcione ni api/Customers ni api.example.com, garantizamos que no habrá ninguna sorpresa con los clientes ni dejarán de funcionar cuando subamos de versión.

El versionado por URI Path nos permite cambiar drásticamente nuestra API porque todo lo que sigue a v<versión> podría cambiar de una versión a otra. Es decir, podríamos pasar de api/v1/Customers, ya no a api/v2/Customers sino a api/v2/Clientes. Lógicamente, esta libertad de cambios supone que los clientes tendrían que actualizar su código para apuntar a las nuevas rutas, y no sólo para cambiar el segmento de la versión sino también para cambiar el resto de la URL.

Además, cualquier bookmark, permalink o similar dejará de funcionar y, si no queremos romper nada, tendremos que configurar nuestra aplicación para devolver un 302 Found o un 301 Moved permanently.

Por otro lado, desde el punto de vista teórico (y poniéndonos la gorra de restafari), tenemos 2 endpoints que, en vez de devolver 2 recursos, devuelven el mismo con una representación distinta. Alguien te dirá que eso no es correcto (no seré yo).

Una solución para intentar atajar globalmente este tipo de versionado en nuestra aplicación sería indicar a la ruta que sólo buscará controladores en un espacio de nombres concreto, pero Web API no permite especificar constraint namespaces en una ruta tal y cómo sí lo hace MVC, luego tendremos que crear una implementación propia de IHttpControllerSelector que entienda rutas con la plantilla api/{namespace}/{controller}/{id} y busque controladores sólo en el espacio de nombres especificado en {namespace}.

La implementación está aquí https://blogs.msdn.microsoft.com/webdev/2013/03/07/asp-net-web-api-using-namespaces-to-version-web-apis/ y bastaría con añadir una ruta por convención y crear los controladores en un espacio de nombres por versión. De este modo tanto api/v1/Customers como api/v2/Customers funcionarían correctamente y no habría ambigüedad en la búsqueda del controlador.

var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{namespace}/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);
config.Services.Replace(typeof(IHttpControllerSelector), 
	new NamespaceHttpControllerSelector(config));

Cada controlador en su espacio de nombres por versión:

namespace ConsoleApplication1.Controllers.v1
{
    public class CustomersController : ApiController
}	
namespace ConsoleApplication1.Controllers.v2
{
    public class CustomersController : ApiController
}

Ya lo dice el post de donde se tomó el ejemplo de IHttpControllerSelector, y es que quizás habría que hacer un fallback a un número de versión anterior si no se encuentra ningún controlador que coincida con el namespace buscado, porque si no cada vez que subiéramos versión tendríamos que copiar todos los controladores de la anterior versión a la nueva, aunque sólo haya cambiado uno. Es decir, si sólo ha cambiado api/v2/Customers quiero que api/v2/Orders siga ejecutando api/v1/Orders y no tener que copiar OrdersControllers al espacio de nombres v2 aunque no haya sufrido ningún cambio. Al final del post veremos como lo hemos resuelto en nuestro caso.

URI Parameter

En este método la versión se especifica como un parámetro de la querystring. Por ejemplo: api/Customers?version=2

Tanto URI Path como URI Parameter podrían ser la única opción disponible si queremos dar soporte a clientes que no pueden manipular las cabeceras de la petición.

Para implementar este tipo de versionado en ASP.NET Web API, tomaremos prestada la idea desde https://www.asp.net/web-api/overview/releases/whats-new-in-aspnet-web-api-21 donde hay un link a un ejemplo para versionar a través de attribute routing http://aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/RoutingConstraintsSample/ReadMe.txt

Tomando el ejemplo como base, lo haremos nuestro e iremos agregando código a medida que vayamos viendo el resto de opciones de versionado (al final todo morirá en filtrar una ruta en función de la presencia de un valor en la petición). Por ahora, sólo queremos controlar api/Customers?version=1 y api/Customers?version=2.

Los métodos de acción quedarían así:

public class CustomersController : ApiController
{
    [VersionedRoute("api/Customers", 1)]
    public IEnumerable<Customer> GetCustomers1()
    {
        return new List<Customer>()
        {
        };
    }
    [VersionedRoute("api/Customers", 2)]
    public IEnumerable<Customer2> GetCustomers2()
    {
    }
}

Código de VersionedRoute (siempre será el mismo con independencia del tipo de versionado):

class VersionedRoute : RouteFactoryAttribute
{
    private readonly int _allowedVersion;
    private const int DefaultVersion = 1;
    public VersionedRoute(string template)
     : this(template, DefaultVersion)
    {
    }
    public VersionedRoute(string template, int allowedVersion)
        : base(template)
    {
        _allowedVersion = allowedVersion;
    }
    public override IDictionary<string,object> Constraints => new HttpRouteValueDictionary
    {
        { "version", new VersionConstraint(_allowedVersion, DefaultVersion) }
    };
}

Código de VersionConstraint:

class VersionConstraint : IHttpRouteConstraint
{
    private readonly int _allowedVersion;
    private readonly int _defaultVersion;
    public VersionConstraint(int allowedVersion, int defaultVersion)
    {
        _allowedVersion = allowedVersion;
        _defaultVersion = defaultVersion;
    }
    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string,object> values, HttpRouteDirection routeDirection)
    {
        if (routeDirection != HttpRouteDirection.UriResolution)
        {
            return false;
        }
        var version = GetVersionFromQueryString(request) ?? _defaultVersion;
        return version == _allowedVersion;
    }
    private static int? GetVersionFromQueryString(HttpRequestMessage request)
    {
        int version;
        if (int.TryParse(GetQueryStringValue(request, "version"), out version))
        {
            return version;
        }
        return null;
    }
    private static string GetQueryStringValue(HttpRequestMessage request, string key)
    {
        var values = request.GetQueryNameValuePairs();
        if (values.All(p => !string.Equals(p.Key, key, StringComparison.OrdinalIgnoreCase)))
        {
            return null;
        }
        return values.Single(p => string.Equals(p.Key, key, StringComparison.OrdinalIgnoreCase)).Value;
    }
}

Custom Header

Con este método la idea es incluir en la petición una cabecera personalizada del estilo X-Version o similar. El prefijo X- es una convención para cabeceras personalizadas, que no son parte del estándar.

Aunque este método no ensucia la URL (separa la información de versión del área de superficie expuesta por nuestra Web Api), el inconveniente es que ya no podremos copiar y pegar la URL, agregar un favorito o pasar la dirección por correcto electrónico. Ahora el cliente tiene que poder enviar una cabecera personalizada en la petición y nosotros, como desarrolladores, tendremos que utilizar Fiddler o una herramienta similar.

Para su implementación, tendremos que modificar la clase VersionConstraint para soporte adicionalmente el versionado por Custom Header.

public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string,object> values, HttpRouteDirection routeDirection)
{
    if (routeDirection != HttpRouteDirection.UriResolution)
    {
        return false;
    }
    var version = GetVersionFromQueryString(request);
    if (version != null)
    {
        return version == _allowedVersion;
    }
    version = GetVersionFromHeaders(request) ?? _defaultVersion;
    return version == _allowedVersion;
}
private static int? GetVersionFromHeaders(HttpRequestMessage request)
{
    IEnumerable<string> headerValues;
    if (request.Headers.TryGetValues("x-version", out headerValues))
    {
        int version;
        if (int.TryParse(headerValues.First(), out version))
        {
            return version;
        }
    }
    return null;
}

Content Negotiation

Se basa en el uso de la cabecera Accept y un MIME Type personalizado donde se especifica que versión del recurso queremos obtener.

Se presenta en 2 distintas formas:

  • application/json; version=1
  • application/vnd.<compañía>.<recurso>+json; version=1

En la primera se usa el tipo MIME estándar y se le agrega un parámetro de versión.

En la segunda se usa un tipo MIME personalizado donde se especifica tanto la versión como el formato deseado.

Por cierto, vnd es de vendor https://en.wikipedia.org/wiki/Media_type

Para implementar ambas, ASP.NET nos ayudará porque cualquier cabecera acepta parámetros y el framework los parsea automáticamente:

public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string,object> values, HttpRouteDirection routeDirection)
{
    if (routeDirection != HttpRouteDirection.UriResolution)
    {
        return false;
    }
    var version = GetVersionFromQueryString(request);
    if (version != null)
    {
        return version == _allowedVersion;
    }
    version = GetVersionFromHeaders(request);
    if (version != null)
    {
        return version == _allowedVersion;
    }
    version = GetVersionFromAcceptHeader(request) ?? _defaultVersion;
    return version == _allowedVersion;
}
private static int? GetVersionFromAcceptHeader(HttpRequestMessage request)
{
    var accept = request.Headers.Accept.SingleOrDefault(a =>; a.Parameters.Any(p => string.Equals(p.Name, "version", StringComparison.OrdinalIgnoreCase)));
    if (accept != null)
    {
        int version;
        if (int.TryParse(accept.Parameters.Single(p =>; string.Equals(p.Name, "version", StringComparison.OrdinalIgnoreCase)).Value, out version))
        {
            return version;
        }
    }
    return null;
}

Cuando se complica la implementación es si queremos usar la segunda opción y queremos seguir aceptando la negociación de contenido. Por ejemplo, y para nuestro tipo Customer, las siguientes peticiones devolverán siempre json (porque es el primer MediaTypeFormatter que está registrado en GlobalConfiguration.Configuration.Formatters).

  • Accept: application/vnd.example.com+json; version=2
  • Accept: application/vnd.example.com+xml; version=2

Si lo primero que hacemos al empezar un proyecto con Web API es eliminar el XmlFormatter o, lo que es lo mismo, sólo devolver json, lo anterior no es un problema. Ahora bien, si tenemos que soportar negociación de contenido, tendremos que agregar un MediaTypeFormatter para cada tipo y para cada representación. Un ejemplo completo se puede ver en http://robertgaut.com/Blog/2007/Four-Ways-to-Version-Your-MVC-Web-API

Como muestra el post, lo hay que hacer es crear un par de MediaTypeFormatter (uno para json y otro para xml) y registrar nuestros tipos. Yo entiendo que, si se opta finalmente por este tipo de versionado, la reflexión sería una solución digna frente a tener que registrar manualmente todos los tipos de nuestra aplicación. En cualquier caso, para nuestro ejemplo:

public static class TypeExtensions
{
    public static Type GetTypeFromIEnumerable(this Type type)
    {
        return IsIEnumerable(type) ? type.GetGenericArguments()[0] : null;
    }
    private static bool IsIEnumerable(Type type)
    {
        return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
    }
}
class TypedXmlMediaTypeFormatter : XmlMediaTypeFormatter
{
    private readonly Type _resourceType;
    public TypedXmlMediaTypeFormatter(Type resourceType, MediaTypeHeaderValue mediaType)
    {
        _resourceType = resourceType;
        SupportedMediaTypes.Clear();
        SupportedMediaTypes.Add(mediaType);
    }
    public override bool CanReadType(Type type)
    {
        return _resourceType == type || _resourceType == type.GetTypeFromIEnumerable();
    }
    public override bool CanWriteType(Type type)
    {
        return _resourceType == type || _resourceType == type.GetTypeFromIEnumerable();
    }
}
class TypedJsonMediaTypeFormatter : JsonMediaTypeFormatter
{
    private readonly Type _resourceType;
    public TypedJsonMediaTypeFormatter(Type resourceType, MediaTypeHeaderValue mediaType)
    {
        _resourceType = resourceType;
        SupportedMediaTypes.Clear();
        SupportedMediaTypes.Add(mediaType);
    }
    public override bool CanReadType(Type type)
    {
        return _resourceType == type || _resourceType == type.GetTypeFromIEnumerable();
    }
    public override bool CanWriteType(Type type)
    {
        return _resourceType == type || _resourceType == type.GetTypeFromIEnumerable();
    }
}

Y el registro:

config.Formatters.Insert(0,
	new TypedXmlMediaTypeFormatter(typeof(Customer),
	new MediaTypeHeaderValue("application/vnd.example.com+xml")));
config.Formatters.Insert(0,
	new TypedJsonMediaTypeFormatter(typeof(Customer),
	new MediaTypeHeaderValue("application/vnd.example.com+json")));
config.Formatters.Insert(0,
	new TypedXmlMediaTypeFormatter(typeof(Customer2),
	new MediaTypeHeaderValue("application/vnd.example.com+xml")));
config.Formatters.Insert(0,
	new TypedJsonMediaTypeFormatter(typeof(Customer2),
	new MediaTypeHeaderValue("application/vnd.example.com+json")));

En este punto ya tenemos un atributo VersionedRoute que ha quedado bastante aparente, pero seguimos sin resolver el problema de cómo proceder cuando subamos de versión para los endpoints que no tienen cambios. Es decir, si decoramos una acción del controlador con VersionedRoute(“…”, 2), ésta sólo responderá cuando el cliente especifica la versión 2. Sin embargo, si mi API actual está en la versión 3 y el anterior endpoint no ha cambiado, no quiero tener que cambiar manualmente todos estos endpoints sin cambios a la versión 3, de hecho, no debería, si lo hago voy a romper con todos los clientes que no actualicen su código a la última versión, luego quiero que ese endpoint sin cambios responda tanto a la versión 3 como a la versión 2.

Para solucionarlo, la idea pasa porque cada endpoint sepa reconocer a cuáles versiones puede hacer fallback sin riesgo alguno. Para ello, usaremos la siguiente clase que a grandes rasgos hace:

  • Establecer el actual número de versión (el valor más alto, en mi caso el endpoint más digievolucionado).
    • Este valor hay que mantenerlo manualmente y además es el número de versión por defecto que se usará cuando en VersionedRoute no lo especifiquemos.
  • Buscar por reflexión rutas versionadas y calcular dinámicamente a que versiones de la misma ruta puede hacer fallback.
internal class Versioning
{
    public const int CurrentVersion = 3;
    public static readonly Lazy<IEnumerable<FallbackRoute>> FallbackRoutes;
    static Versioning()
    {
        FallbackRoutes = new Lazy<IEnumerable<FallbackRoute>>(GetFallbackRoutes);
    }
    private static IEnumerable<FallbackRoute> GetFallbackRoutes()
    {
        var fallbackRoutes = GetFallbackRoutesFromVersionedRoutes();
        foreach (var routeTemplate in fallbackRoutes.Select(p => p.RouteTemplate).Distinct())
        {
            var lastFallbackRouteIndexFound = 0;
            for (var version = CurrentVersion; version > 0; version--)
            {
                if (fallbackRoutes.Any(MatchFallbackRoute(routeTemplate, version)))
                {
                    lastFallbackRouteIndexFound = version;
                    continue;
                }
                fallbackRoutes.Single(MatchFallbackRoute(routeTemplate, lastFallbackRouteIndexFound))
                    .AddFallbackVersion(version);
            }
        }
        return fallbackRoutes;
    }
    private static IEnumerable<FallbackRoute> GetFallbackRoutesFromVersionedRoutes()
    {
        return Assembly.GetExecutingAssembly().GetTypes()
            .SelectMany(t => t.GetMethods())
            .Where(m => m.GetCustomAttributes(typeof(VersionedRoute), false).Length > 0)
            .Select(m =>
            {
                var route = m.GetCustomAttribute<VersionedRoute>();
                return new FallbackRoute(route.Template, route.AllowedVersion)
                    ;
            }).ToList();
    }
    private static Func<FallbackRoute, bool> MatchFallbackRoute(string routeTemplate, int allowedVersion)
    {
        return f => (f.RouteTemplate == routeTemplate) && (f.AllowedVersion == allowedVersion);
    }
    public static FallbackRoute GetFallbackRoute(string routeTemplate, int allowedVersion)
    {
        return FallbackRoutes.Value.SingleOrDefault(MatchFallbackRoute(routeTemplate, allowedVersion));
    }
}

FallbackRoute es una clase que simplemente guardar una ruta y a que versiones atiende:

internal class FallbackRoute
{
    public FallbackRoute(string routeTemplate, int allowedVersion)
    {
        RouteTemplate = routeTemplate;
        AllowedVersion = allowedVersion;
        FallbackVersions = new List();
    }
    public string RouteTemplate { get; }
    public int AllowedVersion { get; }
    public IEnumerable FallbackVersions { get; }

    public bool HasFallbackVersion(int version)
    {
        return FallbackVersions.Contains(version);
    }

    public void AddFallbackVersion(int version)
    {
        ((IList)FallbackVersions).Add(version);
    }
}

Y en VersionedConstraint hay que cambiar el código del método Match para que sepa a qué rutas de fallback responderá:

public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
{
    if (routeDirection != HttpRouteDirection.UriResolution)
    {
        return false;
    }
    var version = GetVersion(request) ?? _defaultVersion;
    var fallbackRoute = Versioning.GetFallbackRoute(route.RouteTemplate, _allowedVersion);
    return version == _allowedVersion || (fallbackRoute != null && fallbackRoute.HasFallbackVersion(version));
}

Con estos cambios y asumiendo que CurrentVersion = 3, funcionarán las siguientes rutas:

  • VersionedRoute(“api/Customers”, 1) responderá a la versión 1.
  • VersionedRoute(“api/Customers”, 3) responderá a la versiones 2 y 3.
  • VersionedRoute(“api/Orders”, 2) responderá a la versiones 2 y 1.
  • VersionedRoute(“api/Orders”) responderá a la versión 3.

El cómo organizar el código en namespaces atendiendo a distintas representaciones versionadas de una misma entidad es cosa de cada uno, es decir, esta solución no obliga (y tampoco facilita) el buscar controladores en un espacio de nombres concreto, eso queda a libre elección del consumidor.

Si por algún motivo te parece una buena solución, te recomendaría usar el código de github https://github.com/panicoenlaxbox/WebApiVersioning donde, seguro, estará la versión más actualizada y que funciona.

Un saludo!

martes, 4 de octubre de 2016

Dejar de seguir la pista a un fichero en git

Una operación bastante habitual en git y que siempre me cuesta recordar, es cómo dejar de seguir la pista a ficheros que quiero ignorar. Es decir, bien los agregamos al control de código fuente por error (no estaban siendo ignorados por .gitignore) o bien era correcto seguirles inicialmente la pista pero en algún momento ya no queremos seguir haciéndolo.

Para dejar de seguir un fichero en git lo que hay que hacer es eliminarlo del index y modificar el fichero .gitignore para no volver a subirlo en un siguiente commit. De este modo, el fichero no será eliminado de nuestro working copy y git ya no le seguirá la pista.

Los comandos necesarios serían algo así:

git rm --cached your_file
git add .
git commit -m "Eliminado fichero de git"

A continuación editamos el fichero .gitignore para agregar el fichero… y ya está!

Un saludo!

martes, 6 de septiembre de 2016

Restaurar y renombrar una base de datos

Restaurar una base de datos desde una copia de seguridad es una práctica habitual pero que, puede resultar en alguna descoordinación entre los nombres de las bases de datos, ficheros físicos y ficheros lógicos que, a posteriori, seguro nos generará cierta frustración por no tener todo a nivel.

Por ello, escribir como hacerlo bien en MS SQL Server (o cómo hacerlo como a mí me funciona), parece una buena copia de seguridad (valga la redundancia) de cómo llevar a cabo el proceso.

Con la siguiente instrucción, podemos ver los nombres lógicos y la ubicación de los ficheros físicos que hay dentro de un backup y que nos servirán después para la instrucción RESTORE.

RESTORE FILELISTONLY FROM DISK = 'C:\Users\sergio.leon\Sergio.bak'

Ahora es momento de restaurar la base de datos (los nombres lógicos ‘Sergio’ y ‘Sergio_log’ es información que nos dio la anterior instrucción):

RESTORE DATABASE [panicoenlaxbox]
FROM DISK = 'C:\Users\sergio.leon\Sergio.bak'
WITH REPLACE,
MOVE 'Sergio' TO 'C:\Users\sergio.leon\panicoenlaxbox.mdf',
MOVE 'Sergio_log' TO 'C:\Users\sergio.leon\panicoenlaxbox_log.ldf'

Sin mover los ficheros, el nombre de los mismos sería el que estuviera asociado en el fichero .bak, que serían Sergio.mdf y Sergio_log.ldf. Además, de no coincidir los nombres de los ficheros con el nombre de la base de datos, podría ser (como en el caso de este ejemplo) que Sergio.mdf y Sergio_log.ldf ya existieran, luego es importante especificar la ubicación y nombre de los ficheros con MOVE.

En este punto, el único problema es que el nombre lógico de los ficheros de la base de datos ‘panicoenlaxbox’ no coincide con el nombre de los ficheros físicos. Para cambiar el nombre lógico:

ALTER DATABASE [panicoenlaxbox] MODIFY FILE (NAME=N'Sergio', NEWNAME=N'panicoenlaxbox')
ALTER DATABASE [panicoenlaxbox] MODIFY FILE (NAME=N'Sergio_log', NEWNAME=N'panicoenlaxbox_log')

Otro problema común (al menos en mi caso) es tener que renombrar una base de datos.

Con el siguiente comando podemos hacerlo, pero el problema es que sólo se renombra la base de datos, no así los ficheros físicos ni los nombres lógicos.

sp_renamedb 'Sergio', 'SergioLeon'

Si hay conexiones abiertas no podrás hacer esto, con el siguiente script matarás estas conexiones:

USE [master];
GO

DECLARE @database SYSNAME = 'Sergio'
DECLARE @kill NVARCHAR(MAX) = '';

SELECT
    @kill = @kill + 'kill ' + CONVERT(VARCHAR(5), session_id) + ';'
FROM
    sys.dm_exec_sessions
WHERE
    database_id = DB_ID(@database);

--PRINT @kill
EXEC sys.sp_executesql @kill;

Para los nombres lógicos ya hemos visto como renombrarlos, pero ¿cómo hacerlo con los ficheros físicos? Pues básicamente, hacer un detach de la base de datos, cambiar el nombre de los ficheros y hacer un attach.

sp_detach_db 'SergioLeon'
--Renombrar los ficheros manualmente en disco
CREATE DATABASE [SergioLeon] ON 
( FILENAME = N'C:\Users\sergio.leon\SergioLeon.mdf' ),
( FILENAME = N'C:\Users\sergio.leon\SergioLeon_log.ldf' )
FOR ATTACH

Un saludo!

jueves, 7 de julio de 2016

Publicar WebJob con WebDeploy

Los websites de Azure (o como quiera que se llamen ahora) son un buen invento. Se publican fácilmente desde Visual Studio y tienen un montón de opciones útiles, entre ellas los WebJobs. Sin embargo, después de crear un slot de staging, algo no funcionaba como esperaba en relación al WebJob. El problema era que el WebJob no formaba parte del proyecto, es decir, se estaba creando a mano desde el portal de Azure y sólo se había hecho inicialmente en “production”, con lo que al hacer el swap de “staging” a “production” se estaba perdiendo y al volver a publicar en staging desde Visual Studio no se agregaba… resultado, un WebJob desaparecido en combate porque forma parte del slot.

Una solución sería crear el WebJob manualmente en ambos entornos (“staging” y “production”) y no olvidar activar la opción “Exclude files from the App_Data folder” durante la publicación a través de Web Deploy (un WebJob se guarda en el directorio App_Data). En cualquier caso, realmente el problema es no haber tratado al WebJob como parte del proyecto. Muy clarificador al respecto esta respuesta en stackoverflow http://stackoverflow.com/a/31079730 “Note it is a bad practice to deploy a WebJob directly and not as part of your website files/repository, this is probably the cause of issues you are having.”

Está claro, el WebJob debe ser parte del proyecto.

Si el WebJob es una aplicación de consola hubiera seguido esta guía https://azure.microsoft.com/en-us/documentation/articles/websites-dotnet-deploy-webjobs/ pero el WebJob es Node.js (que tampoco sé si habrá algo de serie para facilitar la vida, pero en una lectura rápida parecería más orientado sólo a aplicación de consola, lo mismo en 2 días me desdigo…).

Finalmente, la pregunta es ¿Cómo hacer que en App_Data esté el WebJob preparado para su publicación a través de Visual Studio? Pues con nuestro amigo MSBuild.

En mi caso he optado por agregar código al fichero .pubxml (publicación).

Primero, restaurar los paquetes de Node, invocando un target antes de la publicación http://stackoverflow.com/a/12920499

  
    
      CustomBeforePublish;
      $(PipelineDependsOn);
    
  
  
    
  

La verdad es que esto se podría haber evitado usando Grunt, Gulp o similar, que está perfectamente integrado con Visual Studio, pero sólo quería tener la carpeta node_modules cuando fuera a publicar, no antes.

En cualquier caso, con esto me aseguro de que todas las dependencias están ahí, porque lógicamente no pienso en agregar estos ficheros al .csproj ni subirlos al control de código fuente ni nada de eso.

Segundo (y porque la carpeta node_modules no está incluida en el .csproj), copiar estos ficheros en el paquete de publicación http://www.hackthedot.dk/2012/10/adding-extra-files-to-deployment.html

  
    
      
      
        App_Data\jobs\triggered\MyWebJob\node_modules\%(RecursiveDir)%(Filename)%(Extension)
      
    
  
  
    
      IncludeNodeModules;
      $(CopyAllFilesToSingleFolderForPackageDependsOn);
    
  

Con esto se ha salvado la papeleta, ahora el WebJob forma parte del proyecto y la publicación lo tiene en cuenta.

Para “staging”, agregando la variable de entorno WEBJOBS_STOPPED con el valor 1 me aseguro que en “staging” no se va a ejecutar el WebJob https://github.com/projectkudu/kudu/wiki/Web-jobs#configuration-settings

Claro está que no parece la mejor opción, lo suyo sería menos MSBuild, más tarea Exec y algún sistema de build de cliente, pero por ahora vale.

Un saludo!

martes, 1 de marzo de 2016

Binding de listas en ASP.NET MVC

El binding de listas en ASP.NET MVC puede ser algo bastante sencillo y automático (en el caso de tipos simples), o por el contrario algo lioso y poco intuitivo (en el caso de tipos complejos).

Para bindear tipos simples tan sólo hay que seguir la norma HTTP de repetir el nombre del parámetro. Por ejemplo para recibir un IEnumerable foo, podríamos llamarlo con ?foo=Bar&foo=Baz y todo funcionará a la perfección.

Para el caso de tipos complejos, igualmente todo funcionará siempre y cuando el nombre de los parámetros sea el adecuado y en función del ValueProvider que recoja los datos.

Por ejemplo, si esperamos recibir un objeto Person:

    
public class Person
    {
        public string Name { get; set; }
        public IEnumerable<Address> Addresses { get; set; }
    }

    public class Address
    {
        public string City { get; set; }
        public string Country { get; set; }
    }

La url que deberíamos enviar por GET sería:

name=John
&addresses[0].city=Madrid
&addresses[0].country=Spain
&addresses[1].city=New York
&addresses[1].country=USA

Fíjate que la norma es propiedad[índice].propiedad, por ejemplo addresses[0].country

Lo más sencillo es utilizar los helpers de Html en la vista para que los atributos name se generen acorde a esta norma.

Sin embargo, ¿Cómo enviamos un triste objeto Javascript cumpliendo esta norma?

En mi caso usaré jQuery.

Si es por POST no hay ningún problema. Cualquier de las siguientes opciones es válida (la primera funciona por la existencia de JQueryFormValueProvider que sabe interpretar cómo serializa jQuery el payload, y la segunda por JsonValueProvider).

            var data = {
                name: "John",
                addresses: [
                    { city: "Madrid", country: "Spain" },
                    { city: "New York", country: "USA" }
                ]
            };
            $.ajax({
                type: "POST",
                url: url,
                data: data
            });
            data = JSON.stringify(john);
            $.ajax({
                type: "POST",
                url: url,
                data: data,
                contentType: "application/json"
            });

Sin embargo, si queremos enviar ese mismo objeto por GET no funcionará.

Si probamos con el método $.param de jQuery (el que nos recomiendan para obtener un objeto serializado y enviarlo por querystring) el resultado no es el esperado:

$.param({
                name: "John",
                addresses: [
                    { city: "Madrid", country: "Spain" },
                    { city: "New York", country: "USA" }
                ]
            });
name=John
&addresses[0][city]=Madrid
&addresses[0][country]=Spain
&addresses[1][city]=New York
&addresses[1][country]=USA

Se parece bastante a lo espera recibir ASP.NET MVC, pero no es lo mismo. Eso lo entiende JQueryFormValueProvider, pero para GET ese proveedor no actúa.

Para solucionar esto (y este es el propósito del post) podemos usar la siguiente función (donde aumentar el objeto String con format no debería ir ahí, pero para el ejemplo y si no vas a usar la función format en ningún otro sitio, pues podría valer).

        var serialize = (function () {

            if (typeof String.format != 'function') {
                String.format = function () {
                    var format = arguments[0];
                    for (var i = 0; i < arguments.length - 1; i++) {
                        var reg = new RegExp('\\{' + i + '\\}', 'gm');
                        format = format.replace(reg, arguments[i + 1]);
                    }
                    return format;
                };
            }

            function serialize(obj) {
                var s = _serialize(obj);
                if (s !== "") {
                    s = s.substring(1, s.length);
                }
                return s;
            }

            function _serialize(obj, prefix) {
                prefix = prefix || "";
                var s = "";
                if (typeof obj !== 'object') {
                    return String.format('&{0}={1}', encodeURIComponent(prefix), encodeURIComponent(obj));
                }
                for (var prop in obj) {
                    if (!obj[prop]) {
                        continue;
                    }
                    if (Array.isArray(obj[prop])) {
                        for (var index = 0; index < obj[prop].length; index++) {
                            s += _serialize(obj[prop][index], prefix + (prefix ? '.' : '') + prop + '[' + index + ']');
                        }
                    } else if (typeof obj[prop] === "object") {
                        s += _serialize(obj[prop], (prefix ? '.' : "") + prop);
                    } else {
                        s += String.format('&{0}={1}', encodeURIComponent((prefix ? prefix + '.' : '') + prop), encodeURIComponent(obj[prop]));
                    }
                }
                return s;
            }

            return serialize;
        })();

Ahora una llamada con:

serialize({
                name: "John",
                addresses: [
                    { city: "Madrid", country: "Spain" },
                    { city: "New York", country: "USA" }
                ]
            });

Devolverá una query string que al enviar como parámetro en GET será conforme a la norma que establece ASP.NET MVC para el binding de listas de tipos complejos.

name=John
&addresses[0].city=Madrid
&addresses[0].country=Spain
&addresses[1].city=New York
&addresses[1].country=USA

Un saludo!

jueves, 26 de noviembre de 2015

Contratación ¿En qué me estoy equivocando?

Aunque no acostumbro a escribir posts de opinión (básicamente porque creo que mi opinión no aporta mucho en casi ningún tema), hoy haré una merecida excepción porque quiero compartir con el resto una situación que me está sorprendiendo y frustrando a partes iguales y además no parece tener visos de solucionarse en un futuro cercano.

Después de 10 años como empresario/autónomo/auto-empleado, todo el equipo anterior (la friolera de 3) hemos emprendido juntos una nueva aventura en una especie de start-up (la llamo así porque no sé cómo se llama ahora a una empresa de reciente creación, pero bueno, no tenemos futbolín, aunque sí partimos de una situación más o menos estable en la que ya tenemos clientes y un producto en el mercado, así que entiendo que tampoco damos el perfil startapil al 100%).

En cualquier caso, una de las premisas iniciales del proyecto es ampliar el equipo técnico con una nueva incorporación. ¡Un nuevo fichaje! Estoy ilusionado, pasar de 3 a 4 es mucho, para algunos no significará nada, pero para mí supone colocar la cuarta pata a una mesa que espero aguante mucho tiempo.

Las primeras preguntas que tuvimos resolver fueron qué perfil buscar y que sueldo ofrecer.

En cuando al perfil (y después de mucho divagar), apostamos por un full-stack centrado en tecnologías .NET. Es decir, un back-end (ASP.NET MVC, Web API, Entity Framework), pero que se defienda razonablemente bien con Javascript y jQuery. La verdad es que perfil es un clon de nosotros mismos. Esto puede ser bueno o malo según como se mire, pero por ahora apostamos por el músculo y en ahondar en lo que ya sabemos (por ahora los experimentos con gaseosa).

En relación al sueldo, hemos determinado que podemos llegar hasta 33K… incluso negociables, fíjate tú. ¡Esto no puede fallar!, es un buen sueldo, ya sé que no es mega-sueldo pero tampoco es un mierdi-sueldo, yo creo (y si no es verdad me corriges) que para España no está mal.

Además, tenemos un buen horario (que por ahora más o menos cumplimos), de lunes a jueves de 8 a 17:00 y los viernes de 8 a 14:00. A mí me gusta mucho esa frase de “un ritmo de trabajo sostenible y sostenido”, es decir, si el éxito de la nueva empresa depende de que nos quedemos echando horas por sistema, claramente algo está mal.

Y con estos mimbres nos lanzamos al mercado laboral seguros de encontrar a un nuevo compañero…

Y a partir de aquí empieza nuestro vía crucis.

Lo que en principio parecería iba a ser una tarea sencilla se está convirtiendo en todo un dolor de muelas. Podemos argumentar que yo no soy de RRHH, luego probablemente no manejo ciertas variables en una entrevista que seguro debería, pero a grandes rasgos intento ser cordial, respetuoso y crear un ambiente favorable para una conversación distendida (en el fondo incluso me siento culpable por tener que decidir si alguien es apto o no para un trabajo, pero ese es otro tema, es más rollo personal…).

Para la preselección de candidatos hemos optado por una empresa de recruiting (que para ser justos están esforzándose mucho por cumplir las expectativas), luego la gente que finalmente entrevistamos ya ha pasado un filtro previo en el que nos aseguran que la persona tiene una salud mental estable e incluso sabe de programación (al menos su CV así lo dice y en una entrevista con un no-técnico parten la pana). Por otro lado, yo me he preparado las típicas preguntas sacadas del típico post ¿Qué preguntar cuando entrevistas a un candidato? Por ejemplo, ¿Te gusta lo que haces? ¿Te formas? ¿Cuál es el mayor reto al que te has enfrentado? ¿Te gusta trabajar en equipo? (todavía nadie ha contestado que no, pero tiempo al tiempo…) Vamos, que me siento ridículo en esta parte de la entrevista y en ese momento tanto entrevistado como entrevistador seguro lo estamos pasando muy mal.

Como al principio invertimos mucho tiempo en ver gente que tenía importantes suficientes carencias técnicas llegamos a la conclusión de que el filtro previo de la empresa de recruiting tenía que incorporar una pequeña prueba técnica (no para que ellos la evalúen, sino para que nos manden los resultados y así tengamos algo más de contexto antes de decidir si ver o no a la persona).

Esa primera entrevista técnica es la siguiente:

  • POO
    • Diferencias entre una clase abstracta y una interfaz
  • ASP.NET MVC
    • Diferencia entre @Html.Partial y @Html.Action
    • Diferencia entre @helper y @functions
  • ASP.NET WebAPI
    • ¿Para qué sirve [HttpGet] y [HttpPost]?
    • ¿Para qué sirve [FromUri] y [FromBody]?
  • Entity Framework
    • Diferencia entre Database First y Code First
    • ¿Qué hace DbSet.Find?
    • Diferencia entre IEnumerable e IQueryable
    • Lazy loading vs Eager loading vs Explicit loading
  • Javascript/jQuery
    • Hablando de eventos ¿Qué es la delegación?
    • ¿Qué es un closure?
    • ¿Qué es una promise?

Por descontado que no pretendo que nadie me dé una respuesta académica a cada una de las preguntas, pero sí por lo menos que en sus propias palabras conteste algo relacionado con el tema y que tenga cierto sentido.

También cabe decir que inicialmente (en mis mundos de arco iris y unicornios) había preparado un Word con más de 70 preguntas en distintos niveles o fases. Claro ¿Cómo iba a elegir de entre los buenos al mejor sino haciendo un poco de pair-programming con él e intercalando hábilmente preguntas de todo tipo?

Bueno, pues si vierais las respuestas lo fliparíais. Sólo 2 personas de 8 han contestado razonablemente bien. “Razonablemente” es una palabra que uso mucho ahora como medida de protección contra mi creciente frustración.

Ya después de esta selección hay una entrevista (que si puedo siempre hago por Skype). En esta entrevista intento (con más o menos éxito) hacer preguntas técnicas sin que parezca un incómodo interrogatorio. Voy a poner un ejemplo de cómo creo yo debería discurrir la entrevista:

“¿Qué opinas de los genéricos en C#? Y el tema de las lambas, qué movida ¿no? ¿Y cómo te llevas con las promises en Javascript? Oye, seguro que me puedes enseñar algo de código, ¿no? Mira, te voy a enseñar yo este otro código ¿Qué te parece? Y qué importante es el tema de lazy loading vs eager loading vs explicit loading en Entity Framework, ¿verdad? ¿Y los filtros en MVC?”

Y ya si la conversación va viento en popa le pregunto:

“¡Qué locura this en Javascript! ¿verdad? ¡Uff! Como ha cambiado el tema de los ValueProviders y ModelBinders entre ASP.NET MVC y WebAPI, ¡estos de Microsoft nos quieren matar!”

Pues te puedes imaginar, esta entrevista es imaginaria y no ha sucedido… y con franqueza no sé si llegará a suceder alguna vez. Siendo así, la pregunta que me hago es ¿En qué me estoy equivocando?

Si yo fuera mañana a hacer una entrevista de empleo, lo primero que haría sería colgar código en github o similar (si no recuerdo mal me van a querer contratar por mi código – además de por otras softskills, pero no es el tema del post de hoy). También me leería (y por qué no me estudiaría) los típicos posts de “Top interview questions about [inserta aquí tecnología/lenguaje]”.

Es cierto que eso no funcionará porque la entrevista no es un examen, no vale con la memoria a corto plazo, esto va de bagaje personal y de cómo encaras y has encarado la profesión… pero, en cualquier caso, optar a un trabajo de programador.NET en el año 2015 y no saber defender con una mínima coherencia las diferencias entre una interface y una clase abstracta me parece un pecado capital (IMHO). Igualmente, no me digas que haces front-end si no sabes (ni te suena) lo que es promise o closure.

En este punto queda claro que algo está fallando. ¿Posibles reflexiones? Yo hago las siguientes.

¿Tendré una visión distorsionada del mercado laboral y por 33K tengo que contratar a un padawan en vez de a un señor programador? Puede ser, yo creo que no, que es un sueldo majo, pero ya dudo de todo…

¿La gente buena ya está pillada (y cobrando un sueldo igual o superior al ofrecido) y además cambiar la comodidad y seguridad de una antigüedad no les hará atractivo el proyecto?

Yo asumo que quien venga tendrá un periodo de adaptación y no será parte productiva real del equipo hasta que no pase un tiempo. Todos necesitamos un periodo de adaptación, conocer la empresa, sus miserias y virtudes, descubrir el dominio del negocio en el que se está trabajando e incluso aprender a contar hasta tres cuando te toque lidiar con código legacy, porque sí, amigo, de ese código “viejuno” hay mucho, también hay código “fresquito”, nuevo y rimbombante, pero del otro hay más y no podemos renunciar a ello, pero lo que sí tengo claro es que si dices que sabes Code First y no te suena el método OnModelCreating… mal vamos… venga, vale, que usas DataAnnotations, aceptamos barco…

¿Realmente el nivel “general” (nótese el entrecomillado) en España es el que yo creía que era? De nuevo aquí podría ser que tuviera una percepción equivocada respecto a este tema y en esto tiene mucha culpa Twitter, los blogs, ¡la comunidad! Reconozco que lo último es echar la culpa a la comunidad, ¡pobre ella! J pero es que yo quiero en mi equipo gente que quiera salir de su zona de confort, perdón, rectifico, gente que YA haya salido de su zona de confort, y eso está siendo muy difícil, y eso sólo lo veo en la gente de Twitter, en la gente que participa en cualquiera de los bolos de MsCoders, Madrid.NET, Formación Tajamar o similar (y participar es “ir”, “oír”, no significa ser el ponente).

Tampoco pretendo con este post encontrar respuestas a mis problemas (que seguramente serán los problemas de muchos) pero al menos sí espero poder ver a gente conocida y en vez de contarles mi película, que ya la hayan visto y me den su más sincera opinión, yo la agradeceré.

Actualización de hoy mismo: No dejes de leer los comentarios, de veras, son todos muy oportunos y valen más que el propio post. Si tuviera que volver a escribirlo probablemente ya sería otro. El objetivo principal del post (que no era otro sino recabar opiniones y aprendar con ello) se ha cumplido y con creces. He cambiado de opinión en algunos puntos y eso es bueno, no me cuesta reconocerlo, cambio mucho de opinión. Y por cierto, aquí está el enlace de la oferta https://betabeers.com/post/programador-fullstack-net-2379/

Un saludo!

martes, 24 de noviembre de 2015

Crear índices en EF con Fluent API

Crear un índice vía Fluent API ha resultado ser una tarea nada intuitiva y más difícil de lo esperado. Es por ello que lo voy a dejar por escrito y así, siguientes veces, no tendré que navegar por mi código para buscar la chuleta de cómo hacerlo, porque en mi opinión es un código irrecordable y que no nunca logro descubrir a golpe de Intellisense.

Partiendo de una entidad muy sencilla como la siguiente:

class Customer

{

    public int Id { get; set; }

    public string Name { get; set; }

    public string Country { get; set; }

    public DateTime CreatedDate { get; set; }

}

Hacer un índice único por el campo Name sería asi:

modelBuilder

    .Entity<Customer>()

    .Property(t => t.Name)

    .HasMaxLength(250)

    .HasColumnAnnotation(IndexAnnotation.AnnotationName,

        new IndexAnnotation(new IndexAttribute() { IsUnique = true }));

Si esto ya parece un código algo enrevesado, crear un índice por varios campos es aun mas sorpredente porque es imprescindible dar un nombre al índice y una posición a cada campo:

var customer = modelBuilder.Entity<Customer>();

const string indexName = "IX_Customers";

customer

    .Property(t => t.Name)

    .HasMaxLength(250)

    .HasColumnAnnotation(

        IndexAnnotation.AnnotationName,

        new IndexAnnotation(

            new IndexAttribute(indexName, 1)

            {

                IsUnique = true

            }));

customer

    .Property(t => t.Country)

    .HasMaxLength(250)

    .HasColumnAnnotation(

        IndexAnnotation.AnnotationName,

        new IndexAnnotation(

            new IndexAttribute(indexName, 2)

            {

                IsUnique = true

            }));

El poner un tamaño a las propiedades de tipo string es porque si no se mapearán a nvarchar(max) y la creación del índice fallará en Sql Server.

Originalmente el post llevaba hasta aquí, pero el bueno de @gulnor me incito me incitó de mala manera a dar una solución y no sólo exponer un problema, y claro, después de leer su reciente y reflexivo post Código abierto, ecosistema cerrado, me he sentido un poco culpable y no he tenido otra que corresponder con una posible solución para que que así no se diga que no contribuyo (o al menos lo intento) :)

La idea es poder crear un índice sin todo el ruido que genera el código anterior, algo asi:

modelBuilder.Entity<Customer>().Index(p => p.Name, true, "IX_Customers", 1);

modelBuilder.Entity<Customer>().Index(p => p.Country, true, "IX_Customers", 2);

modelBuilder.Entity<Customer>().Index(p => p.CreatedDate);

El código con los métodos extensores es el siguiente (en el que no me gusta el nombre del parámetro genérico T2 y además reconozco abusar de la indentación, pero es por un buen motivo, para facilitar la lectura en el blog):

static class ModelBuilderExtensions

{

    private static void CreateIndex(

        PrimitivePropertyConfiguration property,

        bool isUnique = false,

        string name = null,

        int position = 1)

    {

        var indexAttribute = new IndexAttribute();

        if (!string.IsNullOrWhiteSpace(name))

        {

            indexAttribute = new IndexAttribute(

                name,

                position <= 0 ? 1 : position);

        }

        indexAttribute.IsUnique = isUnique;

        property.HasColumnAnnotation(

            IndexAnnotation.AnnotationName,

            new IndexAnnotation(indexAttribute));

    }

 

    public static void Index<T, T2>(

        this EntityTypeConfiguration<T> entity,

        Expression<Func<T, T2>> propertyExpression,

        bool isUnique = false,

        string name = null,

        int position = 1) where T : class where T2 : struct

    {

        var property = entity.Property(propertyExpression);

        CreateIndex(property, isUnique, name, position);

    }

 

    public static void Index<T, T2>(

        this EntityTypeConfiguration<T> entity,

        Expression<Func<T, T2?>> propertyExpression,

        bool isUnique = false,

        string name = null,

        int position = 1) where T : class where T2 : struct

    {

        var property = entity.Property(propertyExpression);

        CreateIndex(property, isUnique, name, position);

    }

 

    public static void Index<T>(

        this EntityTypeConfiguration<T> entity,

        Expression<Func<T, string>> propertyExpression,

        bool isUnique = false,

        string name = null,

        int position = 1) where T : class

    {

        var property = entity.Property(propertyExpression);

        CreateIndex(property, isUnique, name, position);

    }

}

Ahora sí, post cerrado!

Un saludo!