martes, 30 de mayo de 2017

JSON en SQL Server 2016

Seguro que ya estabas enterado, pero en mi caso ha sido recientemente cuando he descubierto que a partir de SQL Server 2016 se puede trabajar con JSON.

Aunque hay una excelente documentación al respecto en JSON Data (SQL Server), de una forma resumida y con ejemplos que pueda recordar fácilmente, me gustaría contarte que posibilidades tenemos para trabajar con JSON en SQL Server.

Lo primero es que no hay un tipo json, en realidad trabajaremos con nvarchar y todo la magia ocurrirá a través de nuevas clausulas y funciones.

Para organizar el post, voy a plantear un escenario donde, primero importaremos datos desde un fichero .json, a continuación formatearemos en JSON la salida de una consulta SQL, para después hacer consultas SQL sobre una columna que guarda JSON y acabar, finalmente, con un consejo sobre índices para mejorar el rendimiento.

El script SQL necesario para todas las pruebas es el siguiente:

CREATE TABLE [dbo].[OrderLines](
	[Id] [int] NOT NULL,
	[Units] [int] NOT NULL,
	[Price] [decimal](18, 2) NOT NULL,
	[ProductId] [int] NOT NULL,
	[OrderId] [int] NOT NULL
 CONSTRAINT [PK_dbo.OrderLines] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
))
GO
CREATE TABLE [dbo].[Orders](
	[Id] [int] NOT NULL,
	[CreatedDate] [datetime] NOT NULL,
	[Comment] [nvarchar](250) NULL
 CONSTRAINT [PK_dbo.Orders] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
))
GO
CREATE TABLE [dbo].[Products](
	[Id] [int] NOT NULL,
	[Name] [nvarchar](250) NULL
 CONSTRAINT [PK_dbo.Products] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
))
GO
ALTER TABLE [dbo].[OrderLines]  WITH CHECK ADD  CONSTRAINT [FK_dbo.OrderLines_dbo.Orders_OrderId] FOREIGN KEY([OrderId])
REFERENCES [dbo].[Orders] ([Id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[OrderLines] CHECK CONSTRAINT [FK_dbo.OrderLines_dbo.Orders_OrderId]
GO
ALTER TABLE [dbo].[OrderLines]  WITH CHECK ADD  CONSTRAINT [FK_dbo.OrderLines_dbo.Products_ProductId] FOREIGN KEY([ProductId])
REFERENCES [dbo].[Products] ([Id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[OrderLines] CHECK CONSTRAINT [FK_dbo.OrderLines_dbo.Products_ProductId]

Para importar datos desde un fichero .json podemos usar la función OPENROWSET con el parámetro SINGLE_CLOB, que lee y devuelve su contenido como una única fila y columna BulkColumn de tipo nvarchar(max). Además, tendremos que usar la función OPENJSON que convierte JSON en filas y columnas.

Nuestro fichero .json será como sigue

[
  {
    "Id": 1,
    "CreatedDate": "2017-05-29T00:00:00.000",
    "OrderLines": [
      {
        "Id": 1,
        "Units": 1,
        "Price": 1.25,
        "Product": {
          "Id": 1,
          "Name": "Product 1"
        }
      },
      {
        "Id": 2,
        "Units": 2,
        "Price": 2.5,
        "Product": {
          "Id": 2,
          "Name": "Product 2"
        }
      }
    ]
  },
  {
    "Id": 2,
    "CreatedDate": "2017-05-29T00:00:00.000",
    "Comment": "A brief but useful comment",
    "OrderLines": [
      {
        "Id": 3,
        "Units": 3,
        "Price": 3.75,
        "Product": {
          "Id": 1,
          "Name": "Product 1"
        }
      }
    ]
  }
]

Ahora podemos usar OPENROWSET y OPENJSON

SELECT BulkColumn, [key], [value], [type]
FROM OPENROWSET (BULK 'C:\panicoenlaxbox\data.json', SINGLE_CLOB) AS T
CROSS APPLY OPENJSON(BulkColumn)

OPENROWSET devuelve la columna BulkColumn, OPENJSON devuelve las columnas key, value y type.

image

Si en fichero .json tuviera un sólo objeto en vez de un array, la salida nos ayudaría a entender mejor como funciona OPENJSON

{
  "Id": 1,
  "CreatedDate": "2017-05-29T00:00:00.000",
  "OrderLines": [
    {
      "Id": 1,
      "Units": 1,
      "Price": 1.25,
      "Product": {
        "Id": 1,
        "Name": "Product 1"
      }
    },
    {
      "Id": 2,
      "Units": 2,
      "Price": 2.5,
      "Product": {
        "Id": 2,
        "Name": "Product 2"
      }
    }
  ]
}

image

OPENJSON tiene la clausula WITH con la que podemos, de forma explícita, establecer la estructura del resultado devuelto. Por ejemplo, para conseguir un conjunto de filas y columnas sobre la que poder trabajar directamente, ejecutaríamos la siguiente consulta

SELECT
	Orders.Id AS OrderId
   ,Orders.CreatedDate
   ,Orders.Comment
   ,OrderLines.Id AS OrderLineId
   ,OrderLines.Units
   ,OrderLines.Price
   ,Product.Id AS ProductId
   ,Product.[Name] AS ProductName
INTO #Table1
FROM OPENROWSET(BULK 'C:\panicoenlaxbox\data.json', SINGLE_CLOB) AS j
CROSS APPLY OPENJSON(BulkColumn)
WITH (
	Id INT,
	CreatedDate DATETIME '$.CreatedDate',
	Comment NVARCHAR(MAX),
	OrderLines NVARCHAR(MAX) AS JSON
) AS Orders
CROSS APPLY OPENJSON(Orders.OrderLines)
WITH (
	Id INT,
	Units INT,
	Price DECIMAL(18, 2),
	Product NVARCHAR(MAX) AS JSON
) AS OrderLines
CROSS APPLY OPENJSON(OrderLines.Product)
WITH (
	Id INT,
	[Name] NVARCHAR(MAX)
) AS Product    

image

Ahora ya sí podemos insertar estos datos leídos del fichero .json en nuestras tablas

INSERT INTO Products SELECT DISTINCT ProductId, ProductName FROM #table1;
INSERT INTO Orders SELECT DISTINCT OrderId, CreatedDate, Comment FROM #table1;
INSERT INTO OrderLines SELECT DISTINCT OrderLineId, Units, Price, ProductId, OrderId FROM #table1;    

Si hablamos ahora de formatear consultas, tendremos que usar la clausula FOR JSON

SELECT * FROM Orders O
INNER JOIN OrderLines OL ON OL.OrderId = O.Id
INNER JOIN Products P ON OL.ProductId = P.Id
FOR JSON AUTO

Que devuelve

    [{
    "Id": 1,
    "CreatedDate": "2017-05-29T00:00:00",
    "OL": [{
        "Id": 1,
        "Units": 1,
        "Price": 1.25,
        "ProductId": 1,
        "OrderId": 1,
        "P": [{
            "Id": 1,
            "Name": "Product 1"
        }]
    }, {
        "Id": 2,
        "Units": 2,
        "Price": 2.50,
        "ProductId": 2,
        "OrderId": 1,
        "P": [{
            "Id": 2,
            "Name": "Product 2"
        }]
    }]
}, {
    "Id": 2,
    "CreatedDate": "2017-05-29T00:00:00",
    "Comment": "A brief but useful comment",
    "OL": [{
        "Id": 3,
        "Units": 3,
        "Price": 3.75,
        "ProductId": 1,
        "OrderId": 2,
        "P": [{
            "Id": 1,
            "Name": "Product 1"
        }]
    }]
}]

Como probablemente este resultado no nos satisfaga, tendremos que tomar el control con FOR JSON PATH. Por ejemplo, la siguiente consulta devuelve exactamente lo mismo que tiene el fichero .json que usamos al comienzo para importar los datos

SELECT
	O.Id
   ,O.CreatedDate
   ,O.Comment
   ,(SELECT
			OL.Id
		   ,OL.Units
		   ,OL.Price
		   ,OL.ProductId AS 'Product.Id'
		   ,P.[Name] AS 'Product.Name'
		FROM OrderLines OL
		INNER JOIN Products P
			ON OL.ProductId = P.Id
		WHERE OL.OrderId = O.Id
		FOR JSON PATH)
	AS OrderLines
FROM Orders O
FOR JSON PATH

En cuanto a que podemos hacer para consultar datos JSON almacenados en una columna, encontramos varias funciones:

  • ISJSON
  • JSON_VALUE
  • JSON_QUERY
  • JSON_MODIFY

ISJSON valida que el texto es JSON válido

JSON_VALUE extrae un valor desde JSON

JSON_QUERY extrae como texto, un objeto u array desde JSON

JSON_MODIFY permite modificar JSON y devuelve el resultado

Lo más sencillo será agregar una nueva columna para poder jugar con ella

ALTER TABLE Orders
ADD SecurityContext NVARCHAR(MAX);
GO
UPDATE Orders SET SecurityContext = '{"Enabled":true,"Roles":["Salesman","Customer"],"Worflows":[{"Name":"Approval","Priority":1},{"Name":"Rejection","Priority":2}]}'
WHERE Id = 1;
UPDATE Orders SET SecurityContext = '{"Enabled":false}'
WHERE Id = 2;    

Ahora podemos ejecutar las siguientes consultas

--ISJSON valida si es JSON
SELECT ISJSON(SecurityContext) FROM Orders;

--JSON_VALUE extrae un valor desde JSON
--'$.Roles[1]' y 'lax $.Roles[1]' son lo mismo, por defecto es lax
SELECT JSON_VALUE(SecurityContext, '$.Roles[1]') FROM Orders;

--Con strict tendremos una excepción porque el segundo registro no tiene valor
--Property cannot be found on the specified JSON path.
--SELECT JSON_VALUE(SecurityContext, 'strict $.Roles[1]') FROM Orders;

--JSON_QUERY devuelve un objeto o un array
SELECT JSON_QUERY(SecurityContext, '$.Worflows') FROM Orders;
--[{"Name":"Approval","Priority":1},{"Name":"Rejection","Priority":2}]

DECLARE @json NVARCHAR(MAX)
SELECT @json = SecurityContext FROM Orders WHERE Id = 1;
--Modificar una propiedad
SELECT JSON_VALUE(JSON_MODIFY(@json, '$.Enabled', 'false'), '$.Enabled');
--Modificar un elemento de un array
SELECT JSON_QUERY(JSON_MODIFY(@json, '$.Roles[1]', 'Administrador'), '$.Roles');
--["Salesman","Administrador"]

--Agregar un elemento a un array
SELECT JSON_QUERY(JSON_MODIFY(@json, 'append $.Roles', 'Agent'), '$.Roles');
--["Salesman","Customer","Agent"]

Por último, sólo mencionar como podemos crear un índice para que JSON_VALUE lo use. Primero veremos el plan de ejecución de una consulta sin el índice y después como cambia cuando lo incluimos

SELECT JSON_VALUE(SecurityContext, '$.Enabled') FROM Orders 
WHERE JSON_VALUE(SecurityContext, '$.Enabled') = 'true'

image

Ahora agregamos el índice (bueno, en realidad agregamos una columna virtual y después el índice) y ejecutamos de nuevo la consulta para confirmar que lo está usando

ALTER TABLE Orders
ADD SecurityContextEnabled AS JSON_VALUE(SecurityContext,'$.Enabled')
GO
CREATE INDEX IX_Orders_Enabled
ON Orders(SecurityContextEnabled)  
GO
SELECT JSON_VALUE(SecurityContext, '$.Enabled') FROM Orders 
WHERE JSON_VALUE(SecurityContext, '$.Enabled') = 'true'

image

Un saludo!

martes, 21 de marzo de 2017

Equipos, áreas e iteraciones en VSO

Organizar el trabajo es un mal necesario y tarde o temprano cualquier equipo de desarrollo tendrá que intentar abordar esta tarea con cierta rigurosidad. Ahora mismo, me ha tocado a mí y a otros pocos en mi empresa la “excitante” tarea de intentar poner algo de orden en el caos. Ya aviso que no tengo ninguna skill reseñable en cuanto a gestión de proyectos, de hecho, el lema de mi blog incluye la palabra “Desordenada”, fíjate tú, me he ahorrado el disclaimer.

La única herramienta que he usado hasta la fecha para llevar a cabo tan magna tarea ha sido Visual Studio Team Services https://www.visualstudio.com/es/vso/ (o Visual Studio Online o Team Foundation Service como se le conocía anteriormente, ya me han corregido en Twitter https://twitter.com/jc_quijano/status/844173841949691905). El flujo era sencillo, crear un nuevo proyecto y agregar, mejor o peor redactados, un buen saco de PBIs, a partir de ahí y fingiendo hacer SCRUM, organizar el trabajo en sprints y tirar pa’lante.

Lo cierto es que en este post no voy a plantear algo muy distinto, pero sí me he dado cuenta con el paso del tiempo y el notable y variopinto incremento de tareas a realizar, que los valores por defecto que propone VSO podrían no ser los más idóneos. Me ha costado bastante entender la relación entre Work Items, Iterations, Areas y Teams, y como he googleado bastante he pensado que, si lo dejo por escrito, yo mismo podré volver en un tiempo futuro para encontrar aquí el porqué de algunas decisiones. Es decir, si quieres entender cómo funciona VSO, o como creo yo que funciona, en relación a la pestaña “Work”, este es tu post, para todo lo demás recuerda, yo soy programador, no Project Manager.

Para hilar la explicación del post, nuestra empresa se llamará Acme y crearemos un proyecto con el mismo nombre y escogeremos el proceso SCRUM.

Recién creado, el nuevo proyecto contiene lo siguiente:

  • 7 Iterations en una estructura jerárquica. Su nodo principal es una Iteration llamada Acme del que cuelgan 6 Iterations llamadas Sprint 1-6.
  • 1 Area llamada Acme.
  • Un Team con el nombre del proyecto + Team. Es decir, Acme Team.
  • El equipo Acme Team tiene como área por defecto el área Acme y tiene asignadas las 6 iteraciones Sprint 1-6.

Como verás, es todo muy “Acme”, sobredosis de nombre.

En la pestaña Overview del proyecto https://acmecorporation.visualstudio.com/Acme/_admin tenemos los equipos (el segmento Acme/_admin es importante, nos dice que estamos a nivel de proyecto):

clip_image001

En la pestaña Work encontramos Iterations y Areas:

clip_image002

clip_image003

Si navegamos al equipo, podemos ver como el segmento de la url cambia desde Acme/_admin a Acme/Acme%20Team/_admin. Aunque la interfaz nos ayuda a saber dónde estamos, viendo la url salimos de dudas, ahora estamos en el equipo, no el proyecto.

En la pestaña Work del equipo encontramos de nuevo Iterations y Areas:

clip_image005

clip_image006

Lo cierto es que, si no creas más equipos en el proyecto, el trabajo que veas a nivel de proyecto es el mismo que verás a nivel de equipo, es decir, da igual si navegas a Acme/_workitems o a Acme/Acme%20Team/_workitems, la query que hay por debajo es la misma.

En el caso de “Backlog items”:

image

Y en el caso del Sprint 1 (por poner un ejemplo), el cambio más reseñable es que pasamos de Iteration Path Under Acme a Iteration Path Under Acme\Sprint 1

image

Es muy importante el operador de la condición, no es lo mismo Under que =, es una diferencia clave a la que hay que prestar atención.

Insisto en que si sólo hay un equipo no hay mucho donde rascar. Cuando se agoten las iteraciones iniciales (Sprint 1-6), se crea una nueva y se asigna al equipo para que esté disponible. Como el operador de Iteration Path para los elementos superiores “Filters for top level work Items” es “Under”, la nueva iteración se verá automáticamente y nuestra vida será plácida.

Sin embargo, si quieres dar cobijo bajo un mismo proyecto a distintos equipos o incluso a distintos proyectos (teniendo entonces un único proyecto en tu organización), las cosas se complican. Podrías usar etiquetas para organizar tu backlog… yo lo he hecho, lo reconozco, pero entender que es una iteración y un área y que relación mantienen con los Work Items y los equipos, te dará otras posibilidades.

A modo de resumen:

  • Un Work Item es un elemento de trabajo cuya propiedad “Work Item Type” determina de qué tipo es. Un Product Backlog Item es un Work Item… una Task o un Bug también lo son.
  • Un Work Item puede relacionarse con otro Work Item. Da igual si no tiene sentido, se puede. Cierto es que la interfaz nos sugiere/conduce hacía hacer las cosas bien, Epic -> Feature -> Product Backlog Item -> Task, pero si por algún extraño motivo te apetece agregar como hijo de una Task a un PBI, se puede.
  • Un Work Item siempre tiene asignado un “Iteration Path” y un “Area Path”.
  • Una iteración es un evento relacionado con el tiempo, normalmente será un sprint, pero podría ser cualquier otra cosa.
  • Un área es una división lógica del Backlog.
  • Una iteración no tiene nada que ver con un área, son entes separados, sólo se relacionan porque ambos son propiedades de un Work Item.
  • Un equipo tiene acceso a n iteraciones.
  • Un equipo tiene acceso a n áreas.
  • Por ende, un equipo verá los Work Items que tengan asignados esas iteraciones o áreas.

Si, por ejemplo, Acme Corporation tuviera dos productos, Cohetes e Imanes ¿Cómo organizar esto en un solo proyecto?

Lo primero sería crear un área por cada proyecto. Tendríamos 2 nuevas áreas hijas bajo el área inicial raíz “Acme”: Acme\Cohetes y Acme\Imanes. De esta forma, cuando agreguemos un nuevo Work Item le asignaríamos el área adecuada. El backlog empieza a tomar forma, ya podríamos dividirlo con una sencilla query y la condición Area Path = Acme\Cohetes.

Sin embargo, para asignar un área a un Work Item tiene que estar disponible en el equipo. Lo más perverso de la situación es que, aunque no esté el área asignada al equipo, podemos asignar el área a un Work Item… lo que hará que no lo veamos, ¿recuerdas la condición del backlog Area Path = Acme? Lo más sencillo es no hacerlo hasta que el equipo no tenga asignada esa área, momento en el cual la condición del backlog del equipo pasará a ser:

image

Porque esta es la verdad sobre las áreas, se agregarán tantas condiciones de igualdad al backlog como áreas tenga asignadas el equipo. Si queremos cambiar el operador a “Under”, se consigue marcando un área como “Include sub areas”, de este modo, si mañana creáramos Acme\Cohetes\Atómicos no sería necesaria agregar esta nueva área para que fuera visible en el backlog del equipo.

Lo único que queda por comentar sobre las áreas es que el área por defecto del equipo será la que se usará automáticamente cuando creemos un nuevo Work Item.

En este momento ya tenemos áreas, ya tenemos nuestro backlog dividido de forma lógica, pero para hablar de iteraciones primero hay que hablar de equipos.

¿De qué nos sirve tener el backlog dividido si al final lo percibimos todavía como un único e indivisible backlog (no queriendo por otra parte, depender de queries)? Para solucionarlo, crearemos 2 nuevos equipos a igualdad del primer nodo de las áreas lógicas que hemos creado, es decir, nuevo equipo Cohetes y nuevo equipo Imanes. Si asignamos al equipo Cohetes el área Acme\Cohetes (incluyendo sub-áreas) y al equipo Imanes el área Acme\Imanes (incluyendo sub-áreas), ahora cada uno verá en su backlog sólo lo que le concierne. Por otro lado, para ver el backlog completo, en el equipo por defecto simplemente marcamos el área raíz para que incluya sub-áreas.

La distribución quedaría así:

  • Acme Team – Acme (include sub-áreas)
  • Cohetes Team – Acme\Cohetes (include sub-áreas)
  • Imanes Team – Acme\Imanes (include sub-áreas)

Ni que decir tiene, que un usuario puede pertenecer a cualquier número de equipos, podría construir tanto cohetes como imanes.

Vale, ahora sí veo el backlog completo (Acme Team) y por separado el backlog de cohetes (Cohetes Team) y el backlog de Imanes (Imanes Team), pero tristemente hay que ponerse a trabajar… ahora toca el turno de las iteraciones.

Como hemos dicho antes, una iteración es un evento temporal con fechas de inicio y fin opcionales. Admite una estructura jerárquica, por lo que podríamos tener algo como lo siguiente:

  • Acme
    • Release 1
      • Sprint 1
      • Sprint 2

Los valores de Iteration Path para el anterior ejemplo serían Acme, Acme\Release 1, Acme\Release 1\Sprint 1 y Acme\Release 1\Sprint 2.

Para ver las iteraciones en un equipo, el equipo las tiene que tener asignadas. Podríamos asignar a Cohetes Team cualquier iteración de las anteriores, pero lo que no podemos hacer es tener una iteración hija y a la vez una iteración padre, es decir, no podemos asignar Acme\Release 1 y Acme\Release 1\Sprint 1 a la vez, si agregamos el sprint, el release se eliminará (pero si no agregamos el sprint, sí podríamos haber agregado el release, espero haberme explicado…).

Finalmente, en la pestaña Work y en el menú izquierdo aparecerán las iteraciones asignadas (una Current y el resto Futures).

¿Coinciden en el mismo tiempo el Sprint 1 para los 2 equipos? Usa el mismo, sin problemas, la información relativa a la capacidad, burdown chart, etc. es relativa al equipo actual. ¿No coincide? Usa iteraciones distintas. Lo que quiero decir es un tema muy flexible y las condiciones propias de tu negocio te harán tomar una u otra decisión. De nuevo, para ver todas las iteraciones a la vez, en el equipo por defecto deberían estar todas las iteraciones asignadas. La pena aquí es que sólo una iteración será la “Current”, con lo que para ver cómo van nuestros equipos tendremos que movernos entre una Current y varias Futures. Sin embargo, para cada equipo queda muy claro cuáles son sus iteraciones y verán sólo las suyas.

Respecto a iteraciones y en el contexto de un equipo, es importante entender las opciones “Default Iteration” y “Backlog iteration”.

Backlog iteration es la ruta a la iteración raíz a partir de la cual se podrán asignar iteraciones al equipo. Es decir, si la establecemos a Acme\Cohetes (porque creamos una iteración con este nombre), sólo podremos asignar iteraciones por debajo de Acme\Cohetes. Además, esto se traduce en una condición del backlog (no hablo aquí de sprints) como Iteration Path Under “Backlog iteration”. Con esta configuración ya podemos responder de forma precisa a la pregunta ¿Qué ve un equipo en su backlog? Ve Work Items de sus áreas asignadas y en iteraciones iguales o inferiores a “Backlog iteration”.

Además, Backlog iteration también será el valor por defecto para Iteration Path cuando agreguemos un Work Item desde el backlog (en sprints o en cualquier otra iteración, el valor por defecto será la propia iteración).

El campo “Default Iteration” es un poco raro, la verdad, sólo sirve para saber cuál será el valor por defecto para Iteration Path para un nuevo Work Item cuando se agregue desde el widget del dashboard principal o desde una query. Entiendo que aquí no hay contexto, no sabe si está en el backlog o en una iteración y por eso no puede tomar una decisión automáticamente. Por defecto vale @CurrentIteration, que es la iteración “Current”, así está bien, mejor no tocarlo.

Llegados a este punto, ya hemos resuelto como se relacionan Team Projects, Work Items, Teams, Iteration Paths y Areas.

Por sacarle punta al asunto, los inconvenientes que por ahora veo sobre tener equipos por proyecto serían:

  • La velocidad es por proyecto.
  • Si alguien trabaja al mismo tiempo en esprines en distintos equipos, tendrá que dividir su capacidad y estar al tanto de dos esprines, no sólo uno.
  • Con el tiempo y si el número de proyectos crece, podría haber más equipos que gente trabajando. ¡Awesome!

Bueno, espero que le sea de utilidad a alguien este mamotreto, a mí seguro que sí.

¡Un saludo!

domingo, 8 de enero de 2017

Personalizar prompt en Bash/Visual Studio Code

Ahora que la consola mola y he adoptado como animal de compañía a Visual Studio Code, me pasa que estoy más tiempo del previsto en el panel de la terminal integrada.

La verdad es que funciona muy bien y raramente abro ya el terminal como aplicación independiente, pero lo cierto es que en ambas versiones le falta color... y eso es una buena excusa para indagar un poco sobre cómo funciona bash y como personalizarlo

Como básicamente no tengo ningún background linuxero y llevo con Mac 6 meses, lo mismo digo algo por lo que merezco la hoguera, pero yo sólo quería cambiar el prompt y agregar color a la consola, avisado estás.

Para personalizar el prompt tenemos que modificar la variable de entorno PS1, en Bash Shell PS1: 10 Examples to Make Your Linux Prompt like Angelina Jolie está muy bien explicado, pero después de hacer algunas pruebas y desesperado por la sintaxis del color, encontré EzPromptEasy Bash PS1 Generator que permite gráficamente construir un prompt nivel 100.

Screen Shot 2017-01-08 at 20.07.04

Solucionado ya el tema del prompt, ahora queda ver como agregar color al comando ls

Lo primero es establecer la variable CLICOLOR a 1 y después en la variable LSCOLORS establecer qué colores queremos según qué elementos (directorios, ejecutables, etc.). Lógicamente y después de haber encontrado el anterior generador pensé que quizás debía haber uno para este tema y… ¡voilà! allí estaba LSCOLORS Generator

Finalmente quedaría un fichero .bash_profile como el siguiente:.

# get current branch in git repo
function parse_git_branch() {
 BRANCH=`git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/'`
 if [ ! "${BRANCH}" == "" ]
 then
  STAT=`parse_git_dirty`
  echo "[${BRANCH}${STAT}]"
 else
  echo ""
 fi
}

# get current status of git repo
function parse_git_dirty {
 status=`git status 2>&1 | tee`
 dirty=`echo -n "${status}" 2> /dev/null | grep "modified:" &> /dev/null; echo "$?"`
 untracked=`echo -n "${status}" 2> /dev/null | grep "Untracked files" &> /dev/null; echo "$?"`
 ahead=`echo -n "${status}" 2> /dev/null | grep "Your branch is ahead of" &> /dev/null; echo "$?"`
 newfile=`echo -n "${status}" 2> /dev/null | grep "new file:" &> /dev/null; echo "$?"`
 renamed=`echo -n "${status}" 2> /dev/null | grep "renamed:" &> /dev/null; echo "$?"`
 deleted=`echo -n "${status}" 2> /dev/null | grep "deleted:" &> /dev/null; echo "$?"`
 bits=''
 if [ "${renamed}" == "0" ]; then
  bits=">${bits}"
 fi
 if [ "${ahead}" == "0" ]; then
  bits="*${bits}"
 fi
 if [ "${newfile}" == "0" ]; then
  bits="+${bits}"
 fi
 if [ "${untracked}" == "0" ]; then
  bits="?${bits}"
 fi
 if [ "${deleted}" == "0" ]; then
  bits="x${bits}"
 fi
 if [ "${dirty}" == "0" ]; then
  bits="!${bits}"
 fi
 if [ ! "${bits}" == "" ]; then
  echo " ${bits}"
 else
  echo ""
 fi
}

export PS1="\[\e[32m\]\u\[\e[m\]@\[\e[32m\]\h\[\e[m\]:\[\e[35m\]\w\[\e[m\]\[\e[34m\]\`parse_git_branch\`\[\e[m\]\[\e[32m\]\\$\[\e[m\] "
export CLICOLOR=1
export LSCOLORS=exfxcxdxbxegedabagacad
alias ls='ls -GFh'
alias showFiles='defaults write com.apple.finder AppleShowAllFiles YES; killall Finder /System/Library/CoreServices/Finder.app'
alias hideFiles='defaults write com.apple.finder AppleShowAllFiles NO; killall Finder /System/Library/CoreServices/Finder.app'

La terminal de Mac funciona perfecto con este fichero.

Screen Shot 2017-01-08 at 20.19.47

Sin embargo Visual Studio Code no se entera, ¿Por qué? Porque .bash_profile es una cosa y .bashrc es otra completamente distinta

.bash_profile se carga cuando la sesión del terminal se hace bajo un login (cosa que siempre ocurre en Mac cuando abrimos el terminal), mientras que .bashrc se carga cuando el terminal es ejecutado sin un login, y resulta que Visual Studio Code carga la terminal integrada con un non-login shell, Allow integrated terminal to run as login shell #7263, luego no ejecuta .bash_profile y sí .bashrc (entiendo que esto cambiará en alguna update venidera). ¿Solución? Mover el código de .bash_profile a .bashrc y en .bash_profile incluir código para chequear si existe .bashrc y cargarlo (dándonos así igual si la sesión del terminal es con login o sin login).

if [ -f ~/.bashrc ]; then
   source ~/.bashrc
fi

Ahora sí, el terminal integrado de Visual Studio Code muestra nuestra personalización.

Screen Shot 2017-01-08 at 20.16.27

Un saludo!

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!