tag:blogger.com,1999:blog-43648151350562145162024-02-07T01:18:21.363-08:00Programación desordenadaSergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.comBlogger244125tag:blogger.com,1999:blog-4364815135056214516.post-63906754591030829032021-02-14T04:19:00.007-08:002021-11-25T04:25:55.917-08:00Crear un paquete de Python y publicarlo en Azure DevOps con una GitHub action<p>En este post, me gustaría compartir una forma de crear un paquete de Python, publicarlo en un feed privado con una GitHub action y consumirlo en una aplicación cliente.</p>
<p>He dicho "una forma" porque hay otras muchas que igualmente serían válidas, basta con darse una vuela por <a href="https://www.pypa.io/en/latest/">https://www.pypa.io/en/latest/</a> y ver que el estado del arte de publicación de paquetes en Python da para un libro.</p>
<p>Estoy usando PowerShell, así que no te extrañes cuando veas cosas como cd .\directorio\, asumo que son todos comandos con fácil traducción al shell de turno.</p>
<p>Para crear el proyecto, lo único que hay que hacer es crear un entorno virtual dentro de la carpeta del proyecto e instalar las dependencias:</p>
<pre class="brush:bash">
mkdir NameFake
cd .\NameFake\
pipenv shell
pipenv install requests
</pre>
<p>En este momento los únicos ficheros del directorio serán Pipfile y Pipfile.lock</p>
<p>No es necesario, pero será más fácil seguir la receta si creamos un directorio name_fake y dentro un fichero __init__.py vacío y un fichero generator.py con este código:</p>
<pre class="brush:python">
import requests
def get_name():
response = requests.get('https://api.namefake.com/random/random/')
return response.json()['name']
if __name__ == '__main__':
name = get_name()
print(name)
</pre>
<p>Para el tema del versionado de la librería, lo haremos creando el fichero name_fake\version.py</p>
<pre class="brush:python">
VERSION = (0, 0, 1)
__version__ = ".".join([str(x) for x in VERSION])
</pre>
<p>Y actualizaremos __init__.py con:</p>
<pre class="brush:python">
from .version import __version__
</pre>
<p>Podemos testear nuestro código con:</p>
<pre class="brush:bash">
python .\name_fake\generator.py
</pre>
<p>Como queremos crear una librería (un paquete) y publicarlo en un repositorio, crearemos un fichero setup.py con el siguiente contenido (que he dejado al mínimo porque también da para una serie entera de posts)</p>
<pre class="brush:python">
from setuptools import setup, find_packages
import toml
from name_fake import __version__
def get_install_requires():
data = toml.load("Pipfile")
return [package + (version if version != "*" else "")
for package, version in data["packages"].items()]
packages = find_packages()
install_requires = get_install_requires()
setup(
name="NameFake-Generator",
version=__version__,
packages=packages,
install_requires=install_requires
)
</pre>
<p>También necesitaremos instalar la dependencia de toml que estamos usando dentro de setup.py</p>
<pre class="brush:bash">
pipenv install toml --dev
</pre>
<p>La estructura del proyecto ha quedado así:</p>
<pre class="brush:bash">
tree /F
C:.
│ Pipfile
│ Pipfile.lock
│ setup.py
│
└───name_fake
generator.py
version.py
__init__.py
</pre>
<p>Ahora ya podemos crear nuestro paquete con el siguiente comando y encontraremos en la carpeta dist el fichero NameFake_Generator-0.0.1-py3-none-any.whl</p>
<pre class="brush:bash">
python setup.py clean --all bdist_wheel
</pre>
<p>Antes de subir a nuestro feed privado, podemos probar la librería creando un nuevo proyecto (y un nuevo entorno virtual) e instalando el paquete localmente:</p>
<pre class="brush:bash">
cd ..
mkdir NameFakeClient
cd .\NameFakeClient\
pipenv shell
pipenv install ..\NameFake\dist\NameFake_Generator-0.0.1-py3-none-any.whl
</pre>
<p>Creamos un fichero main.py con este código:</p>
<pre class="brush:python">
from name_fake import generator
print(generator.get_name())
</pre>
<p>Y lo ejecutamos con:</p>
<pre class="brush:bash">
python .\main.py
</pre>
<p>Ya tenemos un paquete wheel y hemos probado que funciona correctamente, ahora sí podemos publicarlo en nuestro feed privado, del que, por cierto, voy a asumir que ya está creado en Azure DevOps.</p>
<p>Para publicar el paquete necesitaremos un token de Azure DevOps con permisos de read/write en Packages.</p>
<p>Una vez lo tengamos podremos crear nuestro GitHub action con el siguiente contenido:</p>
<pre class="brush:plain">
name: Upload Python Package
on: [workflow_dispatch]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.7.9'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel pipenv twine
pipenv install --dev
- name: Build
run: |
pipenv run python setup.py bdist_wheel
- name: Upload package
run: |
twine upload dist/* --repository-url https://pkgs.dev.azure.com/<YOUR_ORGANIZATION>/<YOUR_TEAM_PROJECT>/_packaging/<YOUR_FEED>/pypi/upload/ --username ${{ secrets.PRIVATEFEED_USERNAME }} --password ${{ secrets.PRIVATEFEED_PASSWORD }} --skip-existing
</pre>
<p>A propósito de este workflow, cabe mencionar que asume se han creado secretos para el nombre de usuario y contraseña (nombre del token y token) y que el campo --repository-url requiere que cada uno ponga el suyo, claro. Además --skip-existing es útil para que el workflow no falle si intentamos publicar una versión que ya existe en el feed. Por último, en el paso de establecer la versión de Python, siéntete libre de poner la que creas oportuna.</p>
<p>Lógicamente, tanto el proyecto de la librería en local (probablemente con pre-commit) como el workflow deberían incluir pasos extrás como flake8, quizás mypy, pruebas automatizadas, etc.</p>
<p>En cualquier caso y teniendo ya nuestro paquete publicado en un feed de Azure DevOps, el último paso es configurar nuestra aplicación cliente para que pueda descargar paquetes no sólo de PyPI.org, sino también de nuestro feed privado.</p>
<p>Para ello, tendremos que crear un nuevo token con permisos read en Packaging y crear el fichero $env:USERPROFILE\pip\pip.ini con este contenido</p>
<pre class="brush:plain">
[global]
extra-index-url=https://<YOUR_TOKEN_NAME>:<YOUR_TOKEN>@pkgs.dev.azure.com/<YOUR_ORGANIZATION>/<YOUR_TEAM_PROJECT>/_packaging/<YOUR_FEED>/pypi/simple/
</pre>
<p>Además, en el fichero Pipfile debemos agregar</p>
<pre class="brush:plain">
[[source]]
url = "https://pkgs.dev.azure.com/<YOUR_ORGANIZATION>/<YOUR_TEAM_PROJECT>/_packaging/<YOUR_FEED>/pypi/simple/"
verify_ssl = true
name = "<YOUR_FEED>"
</pre>
<p>Fíjate que en el fichero pip.ini (y aunque es cierto que este fichero no está dentro del repositorio de código fuente) la autenticación se guarda en plano. Por otro lado, en Pipfile (que sí es parte del repositorio) no tenemos autenticación en la url (sería un problema mayúsculo subir secretos al repositorio). Si no usamos pipenv, podemos confiar en <a href="https://pypi.org/project/artifacts-keyring/">artifacts-keyring</a> para que nos pida usuario y contraseña de forma interactiva al usar pip install, pero con pipenv no la pide, por eso guardamos el token en pip.ini.</p>
<p>Y con esto ya habríamos publicado una librearía de Python en un feed interno y la estaríamos consumiendo en nuestra apliciación cliente.</p>
<p>Un saludo!</p>
Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com0tag:blogger.com,1999:blog-4364815135056214516.post-7256462656530297772021-01-08T03:35:00.001-08:002021-01-08T03:35:43.511-08:00Exportar Wikis de Azure DevOps a Hugo<p>Evaluando <a href="https://gohugo.io/">Hugo</a> como plataforma de blogging para un futuro, me topé con el tema <a href="https://themes.gohugo.io/hugo-geekdoc/">Geekdoc</a> y entonces me surgió la duda de si era una opción razonable usar Hugo como herramienta para la documentación interna.</p>
<p>Si el proyecto fuera Open-Source, la verdad es que optaría por usar GitHub Pages, pero siendo un repositorio privado toca <a href="https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/about-github-pages">pasar por caja</a> y además y con independencia de si el repositorio es público o privado, las GitHub Pages <a href="https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/about-github-pages#publishing-sources-for-github-pages-sites">son siempre públicas</a>, luego opción descartada.</p>
<p>Por ahora (y lleva así mucho tiempo y no tengo ninguna pega), estamos usando las Wikis de Azure DevOps, las Wikis "aprovisionadas", que son distintas a públicar la wiki como código (ficheros .md directamente desde una carpeta del repositorio, al estilo de GitHub Pages), más información <a href="https://docs.microsoft.com/en-us/azure/devops/project/wiki/provisioned-vs-published-wiki?view=azure-devops">aquí</a>.</p>
<p>Hacer una prueba de concepto con Hugo ha sido fácil, uno de los ganchos para usarlo de hecho, pero yo necesitaba verlo poblado de entradas, con documentación real en vez de <i>lorem ipsum</i>, así que migrar wikis "reales" de Azure DevOps a Hugo era la mejor opción para ver como lucía el tema de Geekdoc y si era usable.</p>
<p>Usando la API de Azure DevOps, ha sido relativamente fácil iterar dentro de una organización por los proyectos, wikis, páginas y descargar sus ficheros adjuntos. Por otro lado, aunque el <a href="https://gohugo.io/getting-started/quick-start/">Getting Started</a> de Hugo va como la seda, donde encontré más problemas fue a la hora de entender como manejar los recursos asociados a una página, a este respecto, el <a href="https://regisphilibert.com/blog/2018/01/hugo-page-resources-and-how-to-use-them/">siguiente enlace</a> me sirvió y me pregunto porque no está incluido en la documentación oficial de Hugo.</p>
<p>El código (en Python, necesito practicar) está publicado <a href="https://github.com/panicoenlaxbox/ADO2Hugo">aquí</a> y he publicado un paquete en <a href="https://pypi.org/project/ado2hugo/">PyPI</a> para que usarlo sea sencillo e indoloro.</p>
<pre class="brush:bash">
pip install ado2hugo
set ORGANIZATION=YOUR_ORGANIZATION
set PATH=YOUR_PATH
ado2hugo YOUR_SIRE_DIRECTORY
</pre>
<p>Un saludo!</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com0tag:blogger.com,1999:blog-4364815135056214516.post-4708358890223763302020-12-07T03:46:00.007-08:002020-12-08T04:02:02.855-08:00Programación mínima, C# vs Python<p>Por puro divertimento, me ha dado por pensar cuál sería el programa mínimo que podría hacer para un compañer@ que me pide descargar una página web a un fichero.</p>
<p>Para esta prueba voy a comparar a C# con Python.</p>
<p>De Python se dice que es un lenguaje expresivo, es decir, que con una única línea de código Python se hace normalmente más que con otro lenguaje de programación (ni lo confirmo ni lo desmiento, yo creo que esta afirmación estás basada más en list comprehension o en como hacer swap de 2 variables).</p>
<p>Por ejemplo, si comparamos un swap de variables en Python y en C#, a priori ganaba Python, pero C# se ha puesto las pilas y empatan en número de líneas:
<pre class="brush:python">
my_var = "my_var"
your_var = "your_var"
my_var, your_var = your_var, my_var # this is expressive
print(my_var, your_var) # your_var my_var
</pre>
<pre class="brush:csharp">
using System;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
var myVar = "myVar";
var yourVar = "yourVar";
(myVar, yourVar) = (yourVar, myVar); // This is expressive too
Console.WriteLine($"{myVar} {yourVar}"); // yourVar myVar
}
}
}
</pre>
<p>Como el rival a batir es Python, veamos como haríamos la descarga de la página a un fichero:</p>
<pre class="brush:python">
from urllib import request
request.urlretrieve("https://www.google.es/", "google.html")
</pre>
<p>2 líneas de código es bien, menos código menos oportunidad de meter bugs.</p>
<p>Ahora en C#:</p>
<pre class="brush:csharp">
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static async Task Main(string[] args)
{
using (var httpClient = new HttpClient())
{
File.WriteAllText("google.html", await httpClient.GetStringAsync("https://www.google.es/"));
}
}
}
}
</pre>
<p>Necesitamos hacer algo, esos malvados pythonistas nos están echando en cara que ellos en 2 líneas han hecho lo mismo que nosotros en 17. Es hora de sacar la artillería pesada, <a href="https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/exploration/top-level-statements">top level statements</a> de C# 9 y .NET 5:</p>
<pre class="brush:csharp">
System.IO.File.WriteAllText("google.html",
await new System.Net.Http.HttpClient().GetStringAsync
("https://www.google.es/"));
</pre>
<p>Ahora el problema lo tiene Python, con C# lo hemos hecho en 1 línea (si no eres muy de C#, simplemente he partido la línea en varias para mejorar su legibilidad). En cualquier caso ¡menos líneas no se podían!</p>
<p>Habiendo escrito el mínimo código viable para llevar a cabo la tan magna tarea encomendada, todavía nos queda distribuir nuestra aplicación. Para Python vamos a usar <a href="https://www.pyinstaller.org/">pyinstaller</a> y para C# directamente la CLI de dotnet.</p>
<p>Con Python ha sido necesario los siguientes pasos (dejando a un lado el tema de los entornos virtuales) y se ha generado un fichero .exe con un tamaño de 7,23 MB</p>
<pre class="brush:bash">
pip install pyinstaller
pyinstaller --onefile main.py
</pre>
<p>Con C# y aprovechándonos del <a href="https://devblogs.microsoft.com/dotnet/app-trimming-in-net-5/">trimming</a>, tenemos un fichero .exe con un tamaño de 4,33 MB (más pequeño que en Python)</p>
<pre class="brush:bash">
dotnet publish --self-contained -c Release -r win-x64 -p:PublishTrimmed=True -p:TrimMode=Link -p:PublishSingleFile=true
</pre>
<p>Bueno, pues me quedo más tranquilo, C# está dando pasos de gigante, hemos sido capaces de escribir el mismo código de Python en menos líneas y con un menor tamaño del distribuible, queda claro que C# es un señor lenguaje y plataforma... y además también expresivo... y Python me gusta mucho, ¡ojo!, es sólo que necesitaba una cabeza de turco.</p>
<p>Un saludo!</p>
Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com0tag:blogger.com,1999:blog-4364815135056214516.post-80364735837131482042020-11-22T04:41:00.009-08:002020-11-22T04:45:25.311-08:00Series temporales en SQL<p>A propósito del post que ha escrito <a href="https://twitter.com/jbautistam/status/1330085329831940096">Jose A Bautista</a>, <a href="http://jbautistam.com/Articulos/Programacion/Series-temporales.htm">Series temporales en SQL</a>, este post es complementario (o casi idéntico y entonces se podría considerar un plagio) para terminar de entender cómo calcular las series temporales, o expresado en un lenguaje más llano, crear intervalos. </p>
<p>El problema al que nos enfrentamos (y resolvió, quiero insistir, <a href="https://twitter.com/jbautistam">Jose A Bautista</a> de forma efectiva) era el siguiente:</p>
<p><i>Saber que productos han tenido stock pero no ventas, en un periodo determinado.</i></p>
<p>El problema del problema (un metaproblema por así decirlo) no es averiguar lo anterior, para eso no es necesario crear series temporales, sino trabajar con un gran volumen de datos y que la consulta siga ejecutándose en un tiempo razonable, por eso crear intervalos ofrece una solución porque, presumiblemente (y depende mucho de la dispersión de los datos), reducirá el conjunto de filas sobre el que se ejecuta la consulta (asumiendo también que crear esta tabla intermedia tiene un coste, tanto en tiempo como en almacenamiento).</p>
<p>El set de datos inicial es el siguiente:</p>
<pre class="brush:sql">CREATE TABLE [dbo].[Sales](
[ProductId] [int] NOT NULL,
[PointOfSaleId] [int] NOT NULL,
[Date] [date] NOT NULL,
[Quantity] [int] NOT NULL
) ON [PRIMARY]
GO
INSERT [dbo].[Sales] ([ProductId], [PointOfSaleId], [Date], [Quantity]) VALUES (1, 1, CAST(N'2020-01-01' AS Date), 1)
GO
INSERT [dbo].[Sales] ([ProductId], [PointOfSaleId], [Date], [Quantity]) VALUES (1, 1, CAST(N'2020-01-02' AS Date), 2)
GO
INSERT [dbo].[Sales] ([ProductId], [PointOfSaleId], [Date], [Quantity]) VALUES (1, 1, CAST(N'2020-01-03' AS Date), 3)
GO
INSERT [dbo].[Sales] ([ProductId], [PointOfSaleId], [Date], [Quantity]) VALUES (1, 1, CAST(N'2020-01-10' AS Date), 10)
GO
INSERT [dbo].[Sales] ([ProductId], [PointOfSaleId], [Date], [Quantity]) VALUES (1, 1, CAST(N'2020-01-11' AS Date), 11)
GO
INSERT [dbo].[Sales] ([ProductId], [PointOfSaleId], [Date], [Quantity]) VALUES (1, 1, CAST(N'2020-01-20' AS Date), 20)
GO
INSERT [dbo].[Sales] ([ProductId], [PointOfSaleId], [Date], [Quantity]) VALUES (2, 1, CAST(N'2020-01-01' AS Date), 1)
GO
INSERT [dbo].[Sales] ([ProductId], [PointOfSaleId], [Date], [Quantity]) VALUES (2, 1, CAST(N'2020-01-02' AS Date), 2)
GO
INSERT [dbo].[Sales] ([ProductId], [PointOfSaleId], [Date], [Quantity]) VALUES (2, 1, CAST(N'2020-01-05' AS Date), 5)
GO
INSERT [dbo].[Sales] ([ProductId], [PointOfSaleId], [Date], [Quantity]) VALUES (2, 2, CAST(N'2020-01-15' AS Date), 15)
GO
INSERT [dbo].[Sales] ([ProductId], [PointOfSaleId], [Date], [Quantity]) VALUES (2, 2, CAST(N'2020-01-16' AS Date), 16)
GO
INSERT [dbo].[Sales] ([ProductId], [PointOfSaleId], [Date], [Quantity]) VALUES (2, 2, CAST(N'2020-01-25' AS Date), 25)
</pre>
<p>Finalmente tenemos los siguientes datos:</p>
<table cellspacing=0 border=1>
<tr>
<td style=min-width:50px>ProductId</td>
<td style=min-width:50px>PointOfSaleId</td>
<td style=min-width:50px>Date</td>
<td style=min-width:50px>Quantity</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-01-2020</td>
<td style=min-width:50px>1</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-02-2020</td>
<td style=min-width:50px>2</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-03-2020</td>
<td style=min-width:50px>3</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-10-2020</td>
<td style=min-width:50px>10</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-11-2020</td>
<td style=min-width:50px>11</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-20-2020</td>
<td style=min-width:50px>20</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-01-2020</td>
<td style=min-width:50px>1</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-02-2020</td>
<td style=min-width:50px>2</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-05-2020</td>
<td style=min-width:50px>5</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-15-2020</td>
<td style=min-width:50px>15</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-16-2020</td>
<td style=min-width:50px>16</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-25-2020</td>
<td style=min-width:50px>25</td>
</tr>
</table>
<p>El siguiente paso es entender la función <a href="https://docs.microsoft.com/es-es/sql/t-sql/functions/row-number-transact-sql?view=sql-server-ver15">ROW_NUMBER</a></p>
<pre class="brush:sql">-- Sin PARTITION, simplemente tenemos un contador
SELECT ROW_NUMBER() OVER (ORDER BY ProductId) AS rn, * FROM Sales
</pre>
<table cellspacing=0 border=1>
<tr>
<td style=min-width:50px>rn</td>
<td style=min-width:50px>ProductId</td>
<td style=min-width:50px>PointOfSaleId</td>
<td style=min-width:50px>Date</td>
<td style=min-width:50px>Quantity</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-01-2020</td>
<td style=min-width:50px>1</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-02-2020</td>
<td style=min-width:50px>2</td>
</tr>
<tr>
<td style=min-width:50px>3</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-03-2020</td>
<td style=min-width:50px>3</td>
</tr>
<tr>
<td style=min-width:50px>4</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-10-2020</td>
<td style=min-width:50px>10</td>
</tr>
<tr>
<td style=min-width:50px>5</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-11-2020</td>
<td style=min-width:50px>11</td>
</tr>
<tr>
<td style=min-width:50px>6</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-20-2020</td>
<td style=min-width:50px>20</td>
</tr>
<tr>
<td style=min-width:50px>7</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-01-2020</td>
<td style=min-width:50px>1</td>
</tr>
<tr>
<td style=min-width:50px>8</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-02-2020</td>
<td style=min-width:50px>2</td>
</tr>
<tr>
<td style=min-width:50px>9</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-05-2020</td>
<td style=min-width:50px>5</td>
</tr>
<tr>
<td style=min-width:50px>10</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-15-2020</td>
<td style=min-width:50px>15</td>
</tr>
<tr>
<td style=min-width:50px>11</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-16-2020</td>
<td style=min-width:50px>16</td>
</tr>
<tr>
<td style=min-width:50px>12</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-25-2020</td>
<td style=min-width:50px>25</td>
</tr>
</table>
<pre class="brush:sql">-- PARTITION BY cambia, así que se reinicia el contador
SELECT ROW_NUMBER() OVER (PARTITION BY ProductId, PointOfSaleId ORDER BY ProductId) AS rn, * FROM Sales
</pre>
<table cellspacing=0 border=1>
<tr>
<td style=min-width:50px>rn</td>
<td style=min-width:50px>ProductId</td>
<td style=min-width:50px>PointOfSaleId</td>
<td style=min-width:50px>Date</td>
<td style=min-width:50px>Quantity</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-01-2020</td>
<td style=min-width:50px>1</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-02-2020</td>
<td style=min-width:50px>2</td>
</tr>
<tr>
<td style=min-width:50px>3</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-03-2020</td>
<td style=min-width:50px>3</td>
</tr>
<tr>
<td style=min-width:50px>4</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-10-2020</td>
<td style=min-width:50px>10</td>
</tr>
<tr>
<td style=min-width:50px>5</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-11-2020</td>
<td style=min-width:50px>11</td>
</tr>
<tr>
<td style=min-width:50px>6</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-20-2020</td>
<td style=min-width:50px>20</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-01-2020</td>
<td style=min-width:50px>1</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-02-2020</td>
<td style=min-width:50px>2</td>
</tr>
<tr>
<td style=min-width:50px>3</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-05-2020</td>
<td style=min-width:50px>5</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-15-2020</td>
<td style=min-width:50px>15</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-16-2020</td>
<td style=min-width:50px>16</td>
</tr>
<tr>
<td style=min-width:50px>3</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-25-2020</td>
<td style=min-width:50px>25</td>
</tr>
</table>
<p>Ahora que ya sabemos como funciona ROW_NUMBER, vamos a centranos en lo que queremos conseguir, el resultado final, los intervalos:</p>
<table cellspacing=0 border=1>
<tr>
<td style=min-width:50px>ProductId</td>
<td style=min-width:50px>PointOfSaleId</td>
<td style=min-width:50px>StartDate</td>
<td style=min-width:50px>EndDate</td>
<td style=min-width:50px>Quantity</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-01-2020</td>
<td style=min-width:50px>01-03-2020</td>
<td style=min-width:50px>6</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-10-2020</td>
<td style=min-width:50px>01-11-2020</td>
<td style=min-width:50px>21</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-20-2020</td>
<td style=min-width:50px>01-20-2020</td>
<td style=min-width:50px>20</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-01-2020</td>
<td style=min-width:50px>01-02-2020</td>
<td style=min-width:50px>3</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-05-2020</td>
<td style=min-width:50px>01-05-2020</td>
<td style=min-width:50px>5</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-15-2020</td>
<td style=min-width:50px>01-16-2020</td>
<td style=min-width:50px>31</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-25-2020</td>
<td style=min-width:50px>01-25-2020</td>
<td style=min-width:50px>25</td>
</tr>
</table>
<p>Para llegar del conjunto inicial de datos al resultado final, la SQL necesaria es:</p>
<pre class="brush:sql">WITH sales_cte AS
(SELECT ProductId,
PointOfSaleId,
Date,
SUM(Quantity) AS Quantity
FROM Sales
GROUP BY ProductId,
PointOfSaleId,
Date),
groups_cte AS
(SELECT ROW_NUMBER() OVER (ORDER BY date) AS row_number,
DATEADD(DAY, -ROW_NUMBER() OVER (PARTITION BY ProductId, PointOfSaleId ORDER BY Date), Date) AS [group],
ProductId,
PointOfSaleId ,
Date,
Quantity
FROM sales_cte)
SELECT ProductId,
PointOfSaleId,
MIN(Date) AS StartDate,
MAX(Date) AS EndDate,
SUM(Quantity) AS Quantity
FROM groups_cte
GROUP BY ProductId,
PointOfSaleId,
[group]
</pre>
<p>La clave de la SQL está en <i>-ROW_NUMBER()</i>, vamos a ver el resultado intermedio y entender así que los días consecutivos están creando un grupo que podremos usar posteriormente:</p>
<pre class="brush:sql">SELECT
ROW_NUMBER() OVER (ORDER BY date) AS row_number,
ProductId,
PointOfSaleId,
Date,
Quantity,
-ROW_NUMBER() OVER (PARTITION BY ProductId, PointOfSaleId ORDER BY Date) AS negative_row_number,
DATEADD(DAY, -ROW_NUMBER() OVER (PARTITION BY ProductId, PointOfSaleId ORDER BY Date), Date) AS [group]
INTO #groups
FROM (SELECT ProductId,
PointOfSaleId,
Date,
SUM(Quantity) AS Quantity
FROM Sales
GROUP BY ProductId,
PointOfSaleId,
Sales.Date) as T;
SELECT * FROM #groups;
</pre>
<table cellspacing=0 border=1>
<tr>
<td style=min-width:50px>row_number</td>
<td style=min-width:50px>ProductId</td>
<td style=min-width:50px>PointOfSaleId</td>
<td style=min-width:50px>Date</td>
<td style=min-width:50px>Quantity</td>
<td style=min-width:50px>negative_row_number</td>
<td style=min-width:50px>group</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-01-2020</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>-1</td>
<td style=min-width:50px>12-31-2019</td>
</tr>
<tr>
<td style=min-width:50px>3</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-02-2020</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>-2</td>
<td style=min-width:50px>12-31-2019</td>
</tr>
<tr>
<td style=min-width:50px>5</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-03-2020</td>
<td style=min-width:50px>3</td>
<td style=min-width:50px>-3</td>
<td style=min-width:50px>12-31-2019</td>
</tr>
<tr>
<td style=min-width:50px>7</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-10-2020</td>
<td style=min-width:50px>10</td>
<td style=min-width:50px>-4</td>
<td style=min-width:50px>01-06-2020</td>
</tr>
<tr>
<td style=min-width:50px>8</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-11-2020</td>
<td style=min-width:50px>11</td>
<td style=min-width:50px>-5</td>
<td style=min-width:50px>01-06-2020</td>
</tr>
<tr>
<td style=min-width:50px>11</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-20-2020</td>
<td style=min-width:50px>20</td>
<td style=min-width:50px>-6</td>
<td style=min-width:50px>01-14-2020</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-01-2020</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>-1</td>
<td style=min-width:50px>12-31-2019</td>
</tr>
<tr>
<td style=min-width:50px>4</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-02-2020</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>-2</td>
<td style=min-width:50px>12-31-2019</td>
</tr>
<tr>
<td style=min-width:50px>6</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-05-2020</td>
<td style=min-width:50px>5</td>
<td style=min-width:50px>-3</td>
<td style=min-width:50px>01-02-2020</td>
</tr>
<tr>
<td style=min-width:50px>9</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-15-2020</td>
<td style=min-width:50px>15</td>
<td style=min-width:50px>-1</td>
<td style=min-width:50px>01-14-2020</td>
</tr>
<tr>
<td style=min-width:50px>10</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-16-2020</td>
<td style=min-width:50px>16</td>
<td style=min-width:50px>-2</td>
<td style=min-width:50px>01-14-2020</td>
</tr>
<tr>
<td style=min-width:50px>12</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-25-2020</td>
<td style=min-width:50px>25</td>
<td style=min-width:50px>-3</td>
<td style=min-width:50px>01-22-2020</td>
</tr>
</table>
<p>Después de esto ya podemos agrupar por el grupo creado (la columna [group]) y usar funciones de agregado para sacar el resto de los datos:</p>
<pre class="brush:sql">SELECT ProductId,
PointOfSaleId,
[group],
MIN(Date) AS StartDate,
MAX(Date) AS EndDate,
SUM(Quantity) AS Quantity
FROM #groups
GROUP BY ProductId,
PointOfSaleId,
[group]
</pre>
<table cellspacing=0 border=1>
<tr>
<td style=min-width:50px>ProductId</td>
<td style=min-width:50px>PointOfSaleId</td>
<td style=min-width:50px>group</td>
<td style=min-width:50px>StartDate</td>
<td style=min-width:50px>EndDate</td>
<td style=min-width:50px>Quantity</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>12-31-2019</td>
<td style=min-width:50px>01-01-2020</td>
<td style=min-width:50px>01-03-2020</td>
<td style=min-width:50px>6</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-06-2020</td>
<td style=min-width:50px>01-10-2020</td>
<td style=min-width:50px>01-11-2020</td>
<td style=min-width:50px>21</td>
</tr>
<tr>
<td style=min-width:50px>1</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-14-2020</td>
<td style=min-width:50px>01-20-2020</td>
<td style=min-width:50px>01-20-2020</td>
<td style=min-width:50px>20</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>12-31-2019</td>
<td style=min-width:50px>01-01-2020</td>
<td style=min-width:50px>01-02-2020</td>
<td style=min-width:50px>3</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>1</td>
<td style=min-width:50px>01-02-2020</td>
<td style=min-width:50px>01-05-2020</td>
<td style=min-width:50px>01-05-2020</td>
<td style=min-width:50px>5</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-14-2020</td>
<td style=min-width:50px>01-15-2020</td>
<td style=min-width:50px>01-16-2020</td>
<td style=min-width:50px>31</td>
</tr>
<tr>
<td style=min-width:50px>2</td>
<td style=min-width:50px>2</td>
<td style=min-width:50px>01-22-2020</td>
<td style=min-width:50px>01-25-2020</td>
<td style=min-width:50px>01-25-2020</td>
<td style=min-width:50px>25</td>
</tr>
</table>
<p>Por acabar con algún dato del mundo real, para el cliente en el que estamos haciendo algunas pruebas la reducción de filas ha sido la siguiente:</p>
<table cellspacing=0 border=1>
<tr>
<td style=min-width:50px></td>
<td style=min-width:50px>Original rows</td>
<td style=min-width:50px>Intervals rows</td>
</tr>
<tr>
<td style=min-width:50px>Stock</td>
<td style=min-width:50px>560.089.291</td>
<td style=min-width:50px>12.236.634</td>
</tr>
<tr>
<td style=min-width:50px>Sales</td>
<td style=min-width:50px>22.981.825</td>
<td style=min-width:50px>16.794.972</td>
</tr>
</table>
<p>Un saludo!</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com0tag:blogger.com,1999:blog-4364815135056214516.post-19701551883407265672019-12-30T03:10:00.003-08:002019-12-30T11:58:42.472-08:00Sitio estático multi-lenguaje con webpack<p>Recientemente, he tenido que configurar una plantilla de sitio web estático (digo plantilla porque supuestamente servirá como scaffolding a futuros proyectos, pero seguro quedará obsoleta en 2 días y habrá que modificarla, veremos a posteriori si sirvió a más de un proyecto). El sitio web no es una SPA, es una MPA (Multiple-Page Application) y, además, era un requisito indispensable la traducción a múltiples lenguajes y que fuera en tiempo de compilación, por SEO. Siendo así, y después de no encontrar nada en google ya hecho y que me gustara, pensé que sería fácil y rápido hacerlo con webpack, pero craso error, me ha llevado más tiempo de lo que suponía y he acabado (no sé si con razón o sin razón), buceando en exceso en la documentación, y probando con el método ensayo-error el resultado de la compilación. La conclusión que saco de todo esto es que en el front me siento como un conductor novel que está configurando un vehículo con un excesivo y apabullante número de extras disponibles. De hecho, y por casualidad, hace unas semanas contesté a esta encuesta <a href="https://stateofjs.com/">https://stateofjs.com/</a> y viendo ahora los resultados más del 50% contestamos que sí, que <i>"La creación de aplicaciones JavaScript es demasiado compleja en este momento"</i> <a href="https://2019.stateofjs.com/opinions/#building_js_apps_overly_complex">https://2019.stateofjs.com/opinions/#building_js_apps_overly_complex</a>, aunque claro, más del 50% eramos full-stack <a href="https://2019.stateofjs.com/demographics/#jobTitle">https://2019.stateofjs.com/demographics/#jobTitle</a> así que lo mismo el problema no es javascript, si no gente que le da a todo y así no se puede (yo me incluyo por si no ha quedado claro).</p><p>En este post, me quiero centrar en las decisiones que he tomado en relación con la MPA y a la traducción. Para el resto es más fácil y seguro leer la documentación de webpack.</p>
<p>El repositorio con la plantilla está en <a href="https://github.com/panicoenlaxbox/webpack-static-site-template">https://github.com/panicoenlaxbox/webpack-static-site-template</a></p>
<p>Si tenemos una MPA, tendremos varios entries y por cada uno de ellos podremos decidir que bundles queremos incluir, la idea está sacada de <a href="https://webpack.js.org/guides/entry-advanced/">https://webpack.js.org/guides/entry-advanced/</a></p> <pre class="brush:js">entry: {
index: [
"./src/index.js",
"./src/styles/index.scss",
"selectric/public/selectric.css"
],
about: ["./src/about.js", "./src/styles/about.scss"]
},
plugins: [
new HtmlWebpackPlugin({
filename: path.join(translation.dist, "index.html"),
template: "src/index.html",
chunks: ["index", "vendor"]
}),
new HtmlWebpackPlugin({
filename: path.join(translation.dist, "about.html"),
template: "src/about.html",
chunks: ["about", "vendor"]
}),
</pre>
<p>Fijarse que cada entry especifica los estilos, .scss por lo que no usa require como dependencia en el .js. Además, cada nueva página debería ir acompañada de una nueva entry y una nueva instancia de <i>HtmlWebpackPlugin</i><a href="https://github.com/jantimon/html-webpack-plugin#generating-multiple-html-files">https://github.com/jantimon/html-webpack-plugin#generating-multiple-html-files</a>.</p>
<p>Por otro lado, ya ha aparecido el objeto translation. Aunque se usa <i>i18n-webpack-plugin</i> para traducir las claves de los ficheros .js, también hay claves de traducción en ficheros .html y ahí el reemplazo lo he resuelto con este otro plugin <i>html-string-replace-webpack-plugin-webpack-4.</i></p>
<pre class="brush:js">new HtmlStringReplace({
patterns: [
{
match: /__(.+?)__/g,
replacement: (match, $1) => translation.translation[$1]
}
]
}),
</pre>
<p>Como que el sitio es estático, quería obtener en la raíz de <i>dist/</i> la versión del lenguaje neutro y luego una carpeta <i><lenguaje>/</lenguaje></i> por cada lenguaje soportado. Para ello, he usado una compilación mútiple (la idea está sacada de <a href="https://survivejs.com/webpack/techniques/i18n/">https://survivejs.com/webpack/techniques/i18n/</a>) y luego unas tareas extras para ajustarlo todo.</p>
<p>Que webpack devuelve una función en vez de un objeto, se puede ver en <a href="https://webpack.js.org/configuration/configuration-types/">https://webpack.js.org/configuration/configuration-types/</a>. Esto da mucho juego y abre distintas posibilidades.</p>
<pre class="brush:js">module.exports = (env, argv) => {
const isProduction = argv.mode === "production";
return translations.map(translation => {
return {
entry: {
</pre>
<p><i>translations</i> es un objeto que lee los mismos ficheros que usa … y agrega algunas propiedaes útil para la compilación.</p>
<pre class="brush:js">[ { language: 'en',
translation:
{ language: 'en',
title: 'My static site template',
message: 'My message' },
default: true,
dist: 'C:\\Temp\\webpack-static-site-template\\dist' },
{ language: 'es',
translation:
{ language: 'es',
title: 'Mi plantilla de sitio estático',
message: 'Mi mensaje' },
default: false,
dist: 'C:\\Temp\\webpack-static-site-template\\dist\\es' } ]
</pre>
<p>Ahora cada vez que webpack emite un bundle lo hace teniendo en cuenta el lenguaje:</p>
<pre class="brush:js">output: {
path: translation.dist,
filename: `[name].${translation.language}${
isProduction ? ".[contenthash]" : ""
}.js`
}
</pre>
<p>Como estamos compilando lo mismo varias veces (con la única diferencia del lenguaje), acabaremos por tener en dist/ algo válido pero un poco feo y repetitivo, se puede mejorar haciendo algunos reemplazos (que por otro lado no me gusta hacer, es como hackear el sistema, de largo es lo que más oscuro me parece).</p>
<pre class="brush:js">
new HtmlStringReplace({
patterns: [
{
match: /(<img src=")(?!(\/\/|https?:\/\/|data:image))/gi,
replacement: (match, $1) => `${$1}/`
}
]
}),
new HtmlStringReplace({
enable: !translation.default,
patterns: [
{
match: /(<link href=")(?!(\/\/|https?:\/\/))/gi,
replacement: (match, $1) => `${$1}../`
},
{
match: /(<script type="text\/javascript" src=".*?)(?=vendor\.)/gi,
replacement: (match, $1) => {
return $1.substring(0, $1.lastIndexOf('"') + 1) + "/";
}
}
]
}),
</pre>
<p>Y borrando por último, lo que no queremos en un hook del ciclo de compilación:</p>
<pre class="brush:js">new EventHooksPlugin({
done: () => {
if (!translation.default) {
exec(`rimraf \"dist/${translation.language}/!(*.html|*.js)\"`);
exec(`rimraf \"dist/${translation.language}/vendor*.js"`);
}
}
})
</pre>
<p>Después de esto, acabaremos con una estructura como la siguiente:</p>
<pre class="brush:bash">│ about.css
│ about.en.js
│ about.html
│ index.css
│ index.en.js
│ index.html
│ vendor.css
│ vendor.js
└───es
about.es.js
about.html
index.es.js
index.html
</pre>
<p>Un saludo!</p></pre>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com8tag:blogger.com,1999:blog-4364815135056214516.post-78801159145518731482019-12-06T08:36:00.002-08:002019-12-06T08:36:47.923-08:00High-order Observables (Angular) y ASP.NET Core<p>
<a href="http://reactivex.io/">ReactiveX</a> es una API para la programación asíncrona con Observables. Su implementación en Javascript es <a href="https://rxjs.dev/">RxJS</a> y en Angular (no sé en otros frameworks), se usa y se usa mucho, no puedes pasarlo por alto. <a href="http://alanpryorjr.com/2019-05-15-rxjs-flattening-operators/">Este post</a> (al que luego volveré a hacer mención) empieza con un párrafo bastante lapidario <i>"Like it or not, rxjs is a critical component of modern Angular development. Although it is perfectly possible to use Angular 2+ without using observables, you lose out on an enormous amount of functionality. The reactive pattern is extremely powerful, and once you get over the, admittedly rather high, learning curve the grass is definitely greener on the other side."</i>. Pues así me siento yo, en mi opinión, Angular es un framework con una curva de aprendizaje medio-alta, si a eso le sumamos Redux, en su sabor Angular vía <a href="https://ngrx.io/">NgRx</a>, la cosa se complica un poco/bastante más, pero todo tiene un denominador común... RxJS. De hecho, el lema de NgRx es <i>"Reactive State for Angular"</i>, y Reactive es sinónimo de RxJS. Queda claro que RxJS debe ser importante.
</p>
<p>
En este sentido, hay un concepto de RxJS que me parece especialmente importante y son los <i>high-order Observables</i>. Normalmente, trabajamos con Observables que emiten valores de tipos básicos (strings, numbers, tipos de usuario, etc.), a estos se les llama <i>first-order Observables</i>, pero si un Observable emite, a su vez, Observables, estamos hablando de <i>high-order Observables</i> (es igual que cuando tenemos una función que recibe o devuelve otra función y hablamos de <i>high-order functions</i>, de ahí habrán sacado el nombre, digo yo).
</p>
<p>
Llegar a encontrarse en el código <i>high-order Observables</i> no es excepcional, bastaría un ejemplo sencillo como el siguiente, donde en la suscripción recibimos un <i>Observable<number></i> y no un <i>number</i> ¿Qué hago yo ahora? Yo quería recibir un <i>number</i>.
</p>
<pre class="brush:js">
import { Observable, of } from "rxjs";
import { map } from "rxjs/operators";
of(1, 2, 3)
.pipe(
map<number, Observable<number>>((n: number) => of(n * n))
)
.subscribe((n: Observable<number>) => console.log(n));
// Observable { _isScalar: true, _subscribe: [Function], value: 1 }
// Observable { _isScalar: true, _subscribe: [Function], value: 4 }
// Observable { _isScalar: true, _subscribe: [Function], value: 9 }
</pre>
<p>
Podemos resolverlo creando una suscripción anidada, pero es un anti-pattern. Lo es porque perdemos el control de cuando cancelar la suscripción anidada y además recuerda (sospechosamente) al famoso <i>call-back hell</i>. Si te encuentras dos <i>susbcribe</i> anidados, tienes un problema. Y no lo digo yo, lo dice <a href="https://blogs.msmvps.com/deborahk/higher-order-observable/">Deborah Kurata</a>, que sabe bastante más que yo.
</p>
<pre class="brush:js">
import { Observable, of } from "rxjs";
import { map } from "rxjs/operators";
of(1, 2, 3)
.pipe(
map<number, Observable<number>>((n: number) => of(n * n))
)
.subscribe((n: Observable<number>) =>
n.subscribe((i: number) => {
console.log(i);
})
);
// 1
// 4
// 9
</pre>
<p>
Y ¡ojo!, no vale hace trampas, aunque no tengamos dos <i>subscribe</i> juntos, hay otras formas más enrevesadas de tener <i>nested suscriptions</i>.
</p>
<pre class="brush:js">
import { Observable, of } from "rxjs";
import { map, tap } from "rxjs/operators";
of(1, 2, 3)
.pipe(
map<number, Observable<number>>((n: number) => of(n * n)),
tap<Observable<number>>((o: Observable<number>) => {
o.subscribe((i: number) => {
console.log(`inner ${i}`);
});
})
)
.subscribe((n: Observable<number>) => console.log(`outer ${n}`));
// inner 1
// outer [object Object]
// inner 4
// outer [object Object]
// inner 9
// outer [object Object]
</pre>
<p>
He aprovechado este último ejemplo para introducir dos nuevas palabras que es importante tener en cuenta, <i>source u outer Observable</i> e <i>inner Observable</i>. El Observable al que nos suscribimos se llama <i>outer Observable</i> y, a los Observables que emite (por eso un <i>high-order Observable</i>) se les llama <i>inner Observable</i>.
</p>
<p>
También quiero aprovechar a hacer el disclaimer, de que en este post seré muy verbose con la firma de los métodos porque es una manía que tengo cuando estoy aprendiendo, es decir, lo anterior es equivalente a este otro código:
</p>
<pre class="brush:js">
of(1, 2, 3)
.pipe(
map(n => of(n * n)),
tap(o => {
o.subscribe((i: number) => {
console.log(`inner ${i}`);
});
})
)
.subscribe(n => console.log(`outer ${n}`));
</pre>
<p>
Volviendo al problema original, ¿cómo trabajar entonces con <i>high-order Observables</i>? Pues hay que convertir un <i>high-order Observable</i> en un <i>first-order Observable</i>. Eso se hace con <i>flattenig</i> (aplastamiento, su traducción más o menos acertada al español). Estos operadores permitirán que consumamos los <i>inner Observable</i> como tipos básicos y, además, y esto es muy importante, gestionarán de forma automática la suscripción y cancelación al <i>inner Observable</i>.
</p>
<p>
Por ejemplo, con <i>concatAll</i> podemos resolver el problema inicial.
</p>
<pre class="brush:js">
import { Observable, of } from "rxjs";
import { map, concatAll } from "rxjs/operators";
of(1, 2, 3)
.pipe(
map<number, Observable<number>>((n: number) => of(n * n)),
concatAll<number>(),
)
.subscribe((n: number) => console.log(n));
// 1
// 4
// 9
</pre>
<p>
Y podemos hacerlo un poco mejor, si usamos el operador <i>concatMap</i>, que es la suma de <i>map</i> y <i>concatAll</i> (al igual que sucede por ejemplo en vanilla Javascript con <i>map</i> y <i>flat</i>, que se combinan en <i><a href="https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Objetos_globales/Array/flatMap">flatMap</a></i>)
</p>
<pre class="brush:js">
import { Observable, of } from "rxjs";
import { map, concatAll, concatMap } from "rxjs/operators";
of(1, 2, 3)
.pipe(
concatMap<number, number>((n: number) => of<number>(n * n))
)
.subscribe((n: number) => console.log(n));
// 1
// 4
// 9
</pre>
<p>
Amigo de <i>concatMap</i>, tenemos también a <i>switchMap</i> (un clásico), <i>mergeMap</i> y <i>exhaustMap</i>.
</p>
<p>
Lo más importante de entender es que todos estos operadores responden a la misma pregunta ¿Qué hacer si el <i>outer Observable</i> vuelve a emitir y el <i>inner Observable</i> todavía está trabajando? Es decir, ¿Qué hacer si se solapan emisiones del <i>outer Observable</i> con el <i>inner Observable</i>? Si pasa el suficiente tiempo entre emisiones del <i>outer Observable</i> como para que la suscripción y cancelación del <i>inner Observable</i> ya haya acabado, podemos poner lo que queramos, da igual, se va a comportar de la misma forma. Sin embargo, si el <i>inner Observable</i> todavía esta trabajando ¿qué hacer con el trabajo actual del <i>inner</i>? Aquí es donde el post que mencionaba al principio <a href="http://alanpryorjr.com/2019-05-15-rxjs-flattening-operators/">http://alanpryorjr.com/2019-05-15-rxjs-flattening-operators/</a> me parece genial porque (sin código, importante y felicito por ello al autor) explica con una analogía de jefe-empleado como se comportará para operador de los mencionados en el hipotético caso de que un jefe (<i>outer Observable</i>) manda tareas (emite) a un empleado (<i>flatennig operator</i>) que todavía no ha acabado la anterior (<i>inner Observable</i>).
</p>
<p>
Si lo llevamos a un escenario más concreto, como llamar a una API si el usuario hace click en un botón (y asumiendo no hemos tenido a bien, deshabilitar el botón después de un click, que sería lo suyo), tenemos lo siguiente:
</p>
<p>
<ul>
<li><i>switchMap</i>. Cancela petición en curso y vuelve a llamar a la API.</li>
<li><i>concatMap</i>. Cuando acabe la petición en curso, hará otra llamada. Es una cola. Ademas, se respetará el orden y no llamada hasta no acabar con la anterior (esto es importante porque queremos garantizar que en el back se procesen en orden, que se hagan las peticiones desde cliente en orden no garantiza que en el back se procesen en el mismo orden, por eso se espera a que termine una petición para lanzar la siguiente).</li>
<li><i>mergeMap</i>. A la vez que la petición en curso, se lanzará una nueva llamada, en paralelo.</li>
<li><i>exhaustMap</i>. No hará nada si hay una petición en curso.</li>
</ul>
</p>
<p>
En este ejemplo <a href="https://stackblitz.com/edit/angular-nkgfyr">https://stackblitz.com/edit/angular-nkgfyr</a> está recogido lo anterior, y usando <a href="https://www.mocky.io/ ">https://www.mocky.io/</a> podemos simular un delay para forzar a que el <i>inner Observable</i> esté trabajando cuando volvamos a hacer click en el botón. Es muy importante tener abierta la pestaña network de las developer tools para ver como se cancelan las peticiones en curso en función del operador elegido. En el ejemplo, se usan <i>switchMapTo</i>, <i>concatMapTo</i> y <i>mergeMapTo</i>, que son iguales a sus versiones sin "To", sólo que no necesitan un parámetro de entrada.
</p>
<p>
Llegando al final y, puesto que hemos cancelado peticiones, ¿Qué podemos hacer en el back para aprovecharnos de este comportamiento? Pues usar <i>CancellationToken</i> y propagarlo en todos los métodos asíncronos que lo permitan (por ejemplo, EF, Dapper, MediatR, etc.). Porque es muy bonito que el cliente cancele una petición (le honra), pero si en back seguimos procesando las peticiones, sólo uno estará haciendo lo correcto, el otro seguirá sin percatarse de que está trabajando para nada y que ya a nadie le importa el resultado (triste pero cierto).
</p>
<p>
En ASP.NET Core (y haciendo una API identica a la que hemos consumido anteriormente), simplemente añadiendo el parámetro <i>CancellationToken</i> al método de acción y pasándoselo a <i>Task.Delay</i> hará la magia de lanzar una excepción del tipo <i>System.Threading.Tasks.TaskCanceledException</i> si el cliente cancela la petición.
</p>
<pre class="brush:csharp">
[HttpGet]
public async Task<ActionResult<IEnumerable<User>>> Get(CancellationToken cancellationToken, int delay = 0)
{
await Task.Delay(delay, cancellationToken);
return new[]
{
new User() {Id = 1, Name = "Sergio", Email = "panicoenlaxbox@gmail.com"},
new User() {Id = 2, Name = "Carmen", Email = "panicoenel20@gmail.com"}
};
}
</pre>
<p>
Un saludo!
</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com1tag:blogger.com,1999:blog-4364815135056214516.post-58052539636497430612019-05-14T09:36:00.001-07:002019-05-15T22:16:45.645-07:00Tooling en SQL<p>No es ningún secreto que la programación con SQL no dispone de un tooling a su altura. Si bien es cierto que tenemos una excelente herramienta como SSMS, por el contrario, si estás acostumbrado al desarrollo en C#, javascript o cualquier otro lenguaje, tarde o temprano querrás imitar el escenario al que estás acostumbrado, esto es, linting, guías de estilo de código, integración continua, etc.</p>
<p>Lógicamente, si usamos SQL sólo de forma esporádica o siempre embebido en C#, el asunto sería distinto, pero si te toca lidiar con un montón de procedimientos almacenados, funciones y demás objetos de SQL Server, en mi opinión se le empiezan a ver las costuras al flujo de desarrollo.</p>
<p>Con sinceridad, tampoco es que la solución propuesta aquí sea la panacea, pero es un intento de poner algo de orden y evitar que lo inevitable suceda. Además, personalmente descarto (por ahora) el uso de herramientas profesionales como <a href="https://www.red-gate.com/">redgate</a>, <a href="https://www.apexsql.com/">ApexSQL</a> o <a href="https://www.devart.com/dbforge/sql/">devart</a>, pero por otro lado, son una incuestionable fuente de inspiración para intentar copiar o imitar (que suena mejor) algunas funcionalidades que pueden ser muy útiles.</p>
<p>El propósito del post será llegar a tener un linter, un formateador y un git hook, todo ello en el contexto de código T-SQL.</p>
<p>Para el linter (y habiendo descartado SonarQube, porque el <a href="https://www.sonarsource.com/products/codeanalyzers/sonartsql.html">analizador de T-SQL</a> es de pago) nos quedan pocas opciones. Lo mejor que hemos encontrado es una extensión de VSCode llamada <a href="https://marketplace.visualstudio.com/items?itemName=tsqllint.tsqllint">tsqlint</a>. Dentro de VSCode funciona muy bien, aunque la única pega que le pongo es que el fichero de configuración donde activamos o desactivamos reglas (.tsqllintrc) tiene que estar en %USERPROFILE%, eso hace un poco más difícil el “clonar el repo y listo”, pero bueno, no habiendo más oferta, doy las gracias por esta herramienta.</p>
<p>Aunque tengamos el linter en VSCode, parece mejor opción instalar globalmente el paquete vía npm con npm install tsqllint -g. Ahora podemos crear el fichero .tsqllintrc con tsqllint --init. Y lo más importante, ahora podemos usar el linter desde línea de comandos (lo que abre la puerta a integración continua).</p>
<p>Para los ejemplos voy a usar <a href="https://www.microsoft.com/en-us/download/details.aspx?id=23654">pubs</a> (que aunque me dicen es antigua, es sencilla y hasta un niño la entiende).</p>
<p>Probemos con una instrucción sencilla:</p>
<pre class="brush:sql">SELECT j.job_id,
j.job_desc,
e.emp_id,
e.fname,
e.lname,
e.job_id,
e.hire_date
FROM jobs j
INNER JOIN employee e ON j.job_id = e.job_id
ORDER BY j.job_desc,
e.fname,
e.lname</pre>
<p>Y al pasar el linter</p>
<pre class="brush:bash">C:\Temp\test>tsqllint example.sql
example.sql(8,7): error schema-qualify : Object name not schema qualified.
example.sql(9,13): error schema-qualify : Object name not schema qualified.
example.sql(12,16): warning semicolon-termination : Statement not terminated with semicolon.
Linted 1 files in 0,2461205 seconds
2 Errors.
1 Warnings</pre>
<p>tssqlint permite <a href="https://github.com/tsqllint/tsqllint#plugins">crear plugins</a> de una forma sencilla y además usa C#, así que no hay excusa. </p>
<p>Nuestro plugin lo que hará es buscar la palabra clave UNION y luego ya darlo como warning, error o no reportarlo, según queramos, eso ya es configuración de cada uno. </p>
<p>El código del plugin (lo relevante) es este:</p>
<pre class="brush:csharp"> public class MyTSqlLintUnionPlugin : IPlugin
{
public void PerformAction(IPluginContext context, IReporter reporter)
{
string line;
var lineNumber = 0;
var reader = new StreamReader(File.OpenRead(context.FilePath));
while ((line = reader.ReadLine()) != null)
{
lineNumber++;
var regex = new Regex(@"\s*UNION\s*", RegexOptions.IgnoreCase);
var match = regex.Match(line);
if (match.Success)
{
var column = match.Index;
reporter.ReportViolation(new RuleViolation(
context.FilePath,
"union",
"UNION is forbidden",
lineNumber,
column,
RuleViolationSeverity.Warning));
}
}
}
}
</pre>
<p>Si ahora pasamos el linter a este SQL, ¡tenemos un UNION no permitido!</p>
<pre class="brush:sql">SELECT 1
UNION
SELECT 2;</pre>
<pre class="brush:bash">C:\Temp\test>tsqllint sergio.sql
Loaded plugin: 'MyTSqlLintPlugin.MyTSqlLintUnionPlugin', Version: '1.0.0.0'
sergio.sql(2,0): warning union : UNION is forbidden.
Linted 1 files in 0,2557418 seconds
0 Errors.
1 Warnings</pre>
<p>Visto esto, el único pero que le saco a la extensibilidad (o a mi implementación, mejor dicho) es el uso de expresiones regulares, lo mismo hoy son una solución, pero ya se sabe que mañana…</p>
<p>En cualquier caso, aceptamos barco, y además integrar tsqllint en SSMS también es posible como una herramienta externa.</p>
<p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2ZBQbFAV5UnjMCJbND_yMe31D_A6VSKILce1aJteAE9demW3h7acJbr9LhZLH6K97Xz8hT5lM4ZXjNnXYCOJ97IDwR4MZSffoJvvn3TuQE_wKhupyc8KN4MUsD6oNRu5A5rV5LAKA6-A/s1600/Sin+t%25C3%25ADtulo.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2ZBQbFAV5UnjMCJbND_yMe31D_A6VSKILce1aJteAE9demW3h7acJbr9LhZLH6K97Xz8hT5lM4ZXjNnXYCOJ97IDwR4MZSffoJvvn3TuQE_wKhupyc8KN4MUsD6oNRu5A5rV5LAKA6-A/s320/Sin+t%25C3%25ADtulo.png" width="320" height="173" data-original-width="490" data-original-height="265" /></a></div></p>
<p>Llegados aquí, ¡tenemos linter!, pero si queremos ir un paso más allá y en vez de trabajar con expresiones regulares, usar el parser que usa el propio SQL Server (para por ejemplo, dar un warning en un <a href="https://www.youtube.com/watch?v=i_cVJgIz_Cs">DELETE sin WHERE</a>, que es una de las <a href="https://rules.sonarsource.com/tsql/RSPEC-1590">reglas de SonarQube</a>, por cierto) podemos hacerlo con <a href="https://www.nuget.org/packages/Microsoft.SqlServer.DacFx.x64/">Microsoft.SqlServer.DacFx.x64</a>. </p>
<pre class="brush:csharp">var parser = new TSql140Parser(true);
using (var reader = new StringReader(@"
SELECT * FROM authors;
DELETE jobs --WHERE min_lvl > 10
;"))
{
var result = parser.Parse(reader, out var errors) as TSqlScript;
foreach (TSqlBatch batch in result.Batches)
{
foreach (var statement in batch.Statements.OfType<DeleteStatement>())
{
if (statement.DeleteSpecification.WhereClause == null)
{
Console.WriteLine("¡DELETE sin WHERE, insensato!");
}
break;
}
}
}</pre>
<p>Si probamos a habilitar o deshabilitar el comentario con la condición del DELETE vemos que funciona. Lógicamente, habría que hacer una herramienta de línea de comandos con un código de salida para poder usarla en la build, pero por ahora con saber que se puede hacer (y como un señor con un parser potente) parece suficiente y posibilita un futuro lleno de oportunidades. </p>
<p>En cuanto al formateo de SQL, es decir, una guía de estilo, tampoco hay mucho donde elegir. Si ves lo que tiene por ejemplo devart, <a href="https://sql-format.com/">https://sql-format.com/</a>, es flipante, sin más, por eso viven de ello. Pero alternativas open-source no hay muchas, o al menos no hay muchas que admitan cierto grado de configuración. En nuestro caso, no hemos decantado por <a href="http://poorsql.com/">http://poorsql.com/</a>, tiene muy buena pinta, pero la verdad es que el proyecto parece un poco abandonado. No obstante, es lo mejor que hemos encontrado y no parece que formatear SQL sea algo que esté cambiando todos los días. </p>
<p>Cabe mencionar que hemos descartado el formateo que hace el <a href="https://marketplace.visualstudio.com/items?itemName=ms-mssql.mssql">plugin oficial</a> de SQL Server en VSCode, por eso, porque sólo lo hace en VSCode. Tiene competencia con este <a href="https://marketplace.visualstudio.com/items?itemName=cymonk.sql-formatter">otro</a>, pero ninguno es invocable por línea de comandos (o al menos yo no sé). </p>
<p>Con el tema del formateo tengo la sensación de que vamos a tener que contentarnos con lo que haya y no pedir peras al olmo. </p>
<p>Usando poorsql pasaríamos de esto (escrito por una persona con muy mala baba, la verdad sea dicha). </p>
<pre class="brush:sql">IF (1 =1 ) BEGIN
SELECT j.job_id, j.job_desc,
e.emp_id, e.fname,
e.lname,e.job_id,
e.hire_date
FROM jobs j INNER
JOIN employee e ON
j.job_id = e.job_id
ORDER BY j.job_desc,
e.fname, e.lname END</pre>
<p>a esto otro</p>
<pre class="brush:sql">IF (1 = 1)
BEGIN
SELECT j.job_id,
j.job_desc,
e.emp_id,
e.fname,
e.lname,
e.job_id,
e.hire_date
FROM jobs j
INNER JOIN employee e ON j.job_id = e.job_id
ORDER BY j.job_desc,
e.fname,
e.lname
END</pre>
<p>Aunque podríamos integrarlo como comando en VSCode y en SSMS como herramienta externa, para finalmente poner todo en orden, usaremos node y los scripts de npm (que además sería equivalente a lo que sucedería en el servidor de integración continua). </p>
<p>Para el git hook de pre-commit vamos a usar <a href="https://github.com/typicode/husky">husky</a>. </p>
<pre class="brush:javascript">"husky": {
"hooks": {
"pre-commit": "node index.js ./**/*.sql"
}
}</pre>
<p>Lo que pasará ahora es que cada vez que hagamos un commit, se va a ejecutar el fichero index.js con un glob pattern para todos los ficheros .sql. </p>
<p>index.js es el encargado de ver en que ficheros .sql ha habido cambios y están en zona de staging, para entonces pasar el linter y el formateador a cada uno de ellos y si algo falla, abortar el commit. </p>
<pre class="brush:javascript">const { execSync } = require("child_process");
const glob = require("glob");
// 0: C:\Program Files\nodejs\node.exe
// 1: C:\Temp\test\index.js
var args = process.argv.slice(2);
args.forEach(function(arg) {
const files = getFiles(arg);
files.forEach(function(file) {
if (!formatFile(file)) {
process.exit(1);
}
if (!lintFile(file)) {
process.exit(1);
}
stageFile(file);
});
});
function getFiles(pattern) {
var files = glob.sync(pattern);
var stagedFiles = getStagedFiles();
return files.filter(file => stagedFiles.includes(file));
}
function formatFile(file) {
console.log(`formatting ${file}`);
return executeCommand(`npm run sqlformat -- -f ${file} -g ${file}`);
}
function lintFile(file) {
console.log(`linting ${file}`);
return executeCommand(`npm run tsqllint -- ${file}`);
}
function stageFile(file) {
console.log(`adding ${file} to index`);
return executeCommand(`git add ${file}`);
}
function executeCommand(command) {
try {
execSync(command);
return true;
} catch (error) {
console.log(`stderr ${error.stdout.toString()}`);
return false;
}
}
function getStagedFiles() {
var output = execSync(
`git diff --cached --name-only --diff-filter=ADMR`
).toString();
return output;
}</pre>
<p>La verdad es que explicado por escrito parecen muchas operaciones a seguir, pero bien montado puede suponer una pequeña mejora en el flujo de trabajo con SQL (aquí tienes el código subido en <a href="https://github.com/panicoenlaxbox/sqlworkflow">github</a>).En cualquier caso, me frustra que algo tan transversal como SQL no tenga (al menos yo no he encontrado nada) otros mecanismos para poder mirar de igual a igual a otras tecnologías en cuanto a tooling se refiere.</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com2tag:blogger.com,1999:blog-4364815135056214516.post-46582652287936460572018-03-06T11:25:00.001-08:002018-03-12T11:47:10.022-07:00Servicios comunes en aplicación de consola .NET Core<p>Si usamos ASP.NET Core, es fácil que estemos acostumbrados a que los servicios comunes de la plataforma estén allí o al menos, que sea muy fácil incluirlos y consumirlos. Sin embargo, con una aplicación de consola monda y lironda, tenemos un triste Main y puede ser que incluir DI, logging o configuración se haga un poco cuesta arriba (al menos a mí se me hizo).</p> <p>En cualquier caso, se tiene que poder hacer, porque ya sabemos que una aplicación ASP.NET Core es al fin y al cabo una aplicación de consola (aunque ASP.NET Core es también un framework y hace uso de IoC para facilitar nuestro trabajo). Sin embargo, con File > New > Project… > Console App estamos solos contra el mundo.</p> <p>Por servicios comunes se entienden DI (bueno, estrictamente hablando un <a href="https://blog.kirei.io/aclarando-conceptos-relacionados-con-la-inyeccion-de-dependencias-96e186cb95d1">contenedor de IoC</a>), logging y configuración, servicios a los que no debemos renunciar en una aplicación de consola, o al menos no renunciar si no queremos.</p> <p><i>Después de escribir el post, <a href="https://twitter.com/iceoverflow/status/971341155878297601">me chivaron por twitter</a> que en .NET Core 2.1 habrá importantes cambios en cuanto a crear un host en una aplicación de consola. Puedes leer más información en <a href="https://jmezach.github.io/2017/10/29/having-fun-with-the-.net-core-generic-host/">Having Fun with the .NET Core Generic Host</a> y desde el <a href="https://github.com/aspnet/Hosting/issues/1163">propio github</a> del equipo de producto.</i></p> <p><i>Yo por mi parte he <a href="https://github.com/panicoenlaxbox/ConsoleApp/blob/master/ConsoleApp2/Program.cs">actualizado el repositorio de github</a> con un ejemplo idéntico al aquí expuesto pero funcionando con el nuevo host genérico, así se pueden comparar ambos. En cualquier caso, el post seguía aquí...</i></p> <p>Lo primero que vamos a ver es la configuración.</p> <pre class="brush:csharp">var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var builder = new ConfigurationBuilder()
.SetBasePath(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location))
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile($"appsettings.{environment}.json", optional: true)
.AddEnvironmentVariables()
.AddCommandLine(args);
</pre>
<p><i>Inicialmente, para establecer la ruta base de la configuración usaba <i>Directory.GetCurrentDirectory()</i>, pero <a href="https://twitter.com/javiercampos/status/971625797625745409">de nuevo en twitter</a> me dijeron que mejor no asumir que el <i>cwd (current working directory)</i> es el mismo directorio que donde esté el ejecutable.</i></p>
<p>Para que no explote nada, tenemos que agregar en la raíz del proyecto un fichero con el nombre appsettings.json, por ejemplo:</p>
<pre class="brush:js">{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Debug"
}
}
},
"Foo": {
"Bar": "Baz"
}
}
</pre>
<p>Es importante poner “Copy if newer” en “Copy to Output Directory” para que se copie el fichero en el directorio \bin.</p>
<p>Además, toca agregar paquetes porque en una aplicación de consola empezamos con un .csproj más limpio que una patena.</p>
<pre class="brush:bash">dotnet add package Microsoft.Extensions.Configuration.FileExtensions
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables
dotnet add package Microsoft.Extensions.Configuration.CommandLine
</pre>
<p>Ahora vamos con el logging.</p>
<pre class="brush:csharp">var loggerFactory = new LoggerFactory()
.AddConsole(configuration.GetSection("Logging:Console"))
.AddDebug();
</pre>
<p>Y de nuevo toca agregar los paquetes oportunos.</p>
<pre class="brush:bash">dotnet add package Microsoft.Extensions.Logging.Console
dotnet add package Microsoft.Extensions.Logging.Debug
</pre>
<p>La verdad es que en este punto ya podríamos usar tanto la configuración como el logging.</p>
<pre class="brush:csharp">var logger = loggerFactory.CreateLogger<Program>();
logger.LogWarning("Hello World!");
</pre>
<p>Sin embargo, nos falta hacer DI para poder sacar todo el jugo a nuestra aplicación de consola.</p>
<p>Forzando un poco el ejemplo y para ver después cómo es posible usar <a href="https://docs.microsoft.com/es-es/aspnet/core/fundamentals/configuration/options">patrón de opciones</a> creamos la clase Foo que recogerá la configuración que agregamos antes en el fichero appsettings.json</p>
<pre class="brush:csharp">class Foo
{
public string Bar { get; set; }
}
</pre>
<p>Ahora ya sí, creamos el contenedor.</p>
<pre class="brush:csharp">IServiceCollection services = new ServiceCollection();
services
.AddSingleton(loggerFactory)
.AddLogging();
services
.AddSingleton(configuration)
.AddOptions()
.Configure<Foo>(configuration.GetSection("Foo"));
var serviceProvider = services.BuildServiceProvider();
</pre>
<p>De nuevo, estos son los paquetes que hay que instalar.</p>
<pre class="brush:bash">dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Options.ConfigurationExtensions
</pre>
<p>En este punto, toda la infraestructura está lista, pero ¿Cómo arrancamos nuestra aplicación de consola? Pues creando una clase App e invocando su método Run (tanto App como Run parecen nombres comúnmente usados pero eres libre de elegir cualquier otro). Lógicamente, nos toca registrar la clase App en el contenedor y resolverla antes de poder usarla.</p>
<p>Todo el código junto y disponible en un <a href="https://github.com/panicoenlaxbox/ConsoleApp/blob/master/ConsoleApp1/Program.cs">repositorio de github</a></p>
<pre class="brush:csharp">using System;
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile($"appsettings.{environment}.json", optional: true)
.AddEnvironmentVariables()
.AddCommandLine(args);
var configuration = builder.Build();
var loggerFactory = new LoggerFactory()
.AddConsole(configuration.GetSection("Logging:Console"))
.AddDebug();
IServiceCollection services = new ServiceCollection();
services
.AddSingleton(loggerFactory)
.AddLogging();
services
.AddSingleton(configuration)
.AddOptions()
.Configure<Foo>(configuration.GetSection("Foo"));
services.AddTransient<App>();
var serviceProvider = services.BuildServiceProvider();
var app = (App)serviceProvider.GetService(typeof(App));
app.Run();
}
}
class Foo
{
public string Bar { get; set; }
}
class App
{
private readonly ILogger<App> _logger;
private readonly Foo _foo;
public App(ILogger<App> logger, IOptions<Foo> foo)
{
_logger = logger;
_foo = foo.Value;
}
public void Run()
{
_logger.LogDebug(_foo.Bar);
}
}
}
</pre>
<p>Un saludo!</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com0tag:blogger.com,1999:blog-4364815135056214516.post-13185019612833104052018-02-26T00:57:00.001-08:002018-02-26T00:59:55.319-08:00Find vs First vs Single<p>Hay varios métodos de LINQ que, a priori, parecen sirven al mismo propósito, pero que al fijarnos en detalle vemos son muy distintos, más si cabe si estamos usando LINQ to Entities, me estoy refiriendo a <em>First</em>, <em>FirstOrDefault</em>, <em>Single</em>, <em>SingleOrDefault</em> y <em>Find</em> (método que de hecho sólo está disponible en <em>DbSet<></em>).</p>
<p>Find es el método que deberíamos casi siempre intentar usar porque es el único que podría evitar un round-trip a la base de datos. Find primero busca en el contexto y si no encuentra nada, es entonces cuando ejecuta una consulta a la base de datos. En caso de ir a base de datos y volverse con las manos vacías, devuelve <em>null</em>. Por contra, <em>Find</em> recibe un <em>params object[] keyValues</em>, con lo que si la clave es compuesta podría no ser muy intuitivo y propenso a errores.</p>
<p>Sólo se me ocurre no usar Find si usamos por ejemplo <em>Include</em>, ya que <em>Include</em> es un método de extensión de <em>IQueryable<></em> y Find devuelve directamente una entidad.</p>
<p>Por otro lado, <em>First</em>, <em>FirstOrDefault</em>, <em>Single</em> y <em>SingleOrDefault</em> siempre ejecutarán una consulta a la base de datos, aunque la entidad que fueran a materializar estuviera ya disponible en el contexto. Que siempre vaya a la base de datos no significa que devuelva los valores que estén en la base de datos ya que, de estar disponible en el contexto, nos devolverá la instancia previamente materializada, esto es lo que se denomina en terminología de ORMs, <a href="https://martinfowler.com/eaaCatalog/identityMap.html">IdentityMap.</a></p>
<p>Otra gran diferencia es que <em>Single</em> lanzará una excepción si encuentra 2 o más registros, así que no hay se debería usar <em>First</em> cuando en realidad se quería usar <em>Single</em> o, mejor aún, <em>Find</em>.</p>
<p>Con el siguiente código se puede ver todo lo expuesto.</p>
<pre class="brush:csharp">using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
using (var context = new ShopContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.Orders.Add(new Order() { Id = 1, CustomerPurchaseOrder = "foo" });
context.Orders.Add(new Order() { Id = 2, CustomerPurchaseOrder = "foo" });
context.SaveChanges();
}
using (var context = new ShopContext())
{
var order = context.Orders.Find(1);
//exec sp_executesql N'SELECT TOP(1) [e].[Id], [e].[CustomerPurchaseOrder]
//FROM [Orders] AS [e]
//WHERE [e].[Id] = @__get_Item_0',N'@__get_Item_0 int',@__get_Item_0=1
order.CustomerPurchaseOrder = "panicoenlaxbox"; // Identity Map
order = context.Orders.Find(1); // no db query
Console.WriteLine(order.CustomerPurchaseOrder); //panicoenlaxbox
order = context.Orders.Find(3);
//exec sp_executesql N'SELECT TOP(1) [e].[Id], [e].[CustomerPurchaseOrder]
//FROM [Orders] AS [e]
//WHERE [e].[Id] = @__get_Item_0',N'@__get_Item_0 int',@__get_Item_0=3
Console.WriteLine(order == null); // True
order = context.Orders.First(o => o.Id == 1);
//SELECT TOP(1) [o].[Id], [o].[CustomerPurchaseOrder]
//FROM [Orders] AS [o]
//WHERE [o].[Id] = 1
Console.WriteLine(order.CustomerPurchaseOrder); //panicoenlaxbox
try
{
order = context.Orders.First(o => o.Id == 3);
}
catch (Exception e)
{
Console.WriteLine(e.Message); // Sequence contains no elements
}
order = context.Orders.FirstOrDefault(o => o.Id == 3);
//SELECT TOP(1) [o].[Id], [o].[CustomerPurchaseOrder]
//FROM [Orders] AS [o]
//WHERE [o].[Id] = 2
Console.WriteLine(order == null); // True
order = context.Orders.Single(o => o.Id == 1);
//SELECT TOP(2) [o].[Id], [o].[CustomerPurchaseOrder]
//FROM [Orders] AS [o]
//WHERE [o].[Id] = 1
Console.WriteLine(order.CustomerPurchaseOrder); //panicoenlaxbox
try
{
order = context.Orders.Single(o => o.Id == 3);
}
catch (Exception e)
{
Console.WriteLine(e.Message); // Sequence contains no elements
}
try
{
order = context.Orders.Single(o => o.CustomerPurchaseOrder == "foo");
}
catch (Exception e)
{
Console.WriteLine(e.Message); // Sequence contains more than one element
}
order = context.Orders.SingleOrDefault(o => o.Id == 1);
//SELECT TOP(2) [o].[Id], [o].[CustomerPurchaseOrder]
//FROM [Orders] AS [o]
//WHERE [o].[Id] = 1
Console.WriteLine(order.CustomerPurchaseOrder); //panicoenlaxbox
}
}
}
class ShopContext : DbContext
{
public DbSet<Order> Orders { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer(@"Server=(LocalDB)\MSSQLLocalDB;Database=Shop;Trusted_Connection=True");
}
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>().Property(o => o.Id).ValueGeneratedNever();
base.OnModelCreating(modelBuilder);
}
}
internal class Order
{
public int Id { get; set; }
public string CustomerPurchaseOrder { get; set; }
}
}
</pre>
<p>Un saludo!</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com1tag:blogger.com,1999:blog-4364815135056214516.post-65390230742588709042018-01-31T10:16:00.001-08:002018-01-31T10:18:35.672-08:00SQL dinámico con ExpandoObject y Dapper<p>Crear una consulta SQL con una condición de filtrado dinámico es algo que tarde o temprano sale a la palestra</p> <p>En la solución que se muestra a continuación, se usa un objeto de tipo ExpandoObject que permite agregar propiedades en tiempo de ejecución. Lo explica muy bien <a href="https://twitter.com/eiximenis">@eiximenis</a> en el post <a href="https://geeks.ms/etomas/2010/04/29/var-object-y-dynamic/">Var, object y dynamic</a></p> <p>Si a ExpandoObject le sumamos <a href="https://github.com/StackExchange/Dapper">Dapper</a>, la solución es segura contra SQL injection y flexible en cuanto a su confección</p> <p>En el ejemplo he procurado que sea vea que podemos tanto agregar propierdades de forma dinámica por el mero hecho de asignar un valor, así como castear el ExpandoObject a un dicccionario para agregar propiedades de forma dinámica</p> <p>Primero el código SQL para crear una tabla y que el ejemplo funcione</p> <pre class="brush:sql">
CREATE TABLE [dbo].[Table_1](
	[Id] [int] NOT NULL,
	[TenantId] [int] NOT NULL,
	[C1] [int] NULL,
	[C2] [nvarchar](50) NULL,
	[C3] [datetime2](7) NULL,
CONSTRAINT [PK_Table_1] PRIMARY KEY CLUSTERED
(
	[Id] ASC
)
</pre>
<p>Ahora el código</p>
<pre class="brush:csharp">
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Dynamic;
using System.Linq;
using Dapper;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
QueryExample(new Dictionary<string, object>()
{
{ "C1", 1 },
{ "C2", "Foo" },
{ "C3", DateTime.Now }
});
}
private static void QueryExample(IDictionary<string, object> criterias)
{
const string connectionString = @"Server=(LocalDB)\MSSQLLocalDB;Database=Example;Trusted_Connection=True;";
var sql = "SELECT * FROM Table_1 WHERE TenantId = @TenantId";
var columnsWhitelist = new[] { "C1", "C2", "C3" };
foreach (var criteria in criterias)
{
if (!columnsWhitelist.Contains(criteria.Key))
{
throw new ArgumentException($"{criteria.Key} is not allowed as column name", nameof(criteria));
}
sql += $" AND {criteria.Key} = @{criteria.Key}";
}
dynamic parameters = new ExpandoObject();
parameters.TenantId = 1;
var dictionary = (IDictionary<string, object>)parameters;
foreach (var criteria in criterias)
{
dictionary.Add(criteria.Key, criteria.Value);
}
using (var connection = new SqlConnection(connectionString))
{
connection.Query(sql, (ExpandoObject)parameters);
}
Console.ReadKey();
}
}
}
</pre>
<p>El SQL que se ejecuta finalmente es el siguiente</p>
<pre class="brush:sql">
exec sp_executesql N'SELECT * FROM Table_1 WHERE TenantId = @TenantId AND C1 = @C1 AND C2 = @C2 AND C3 = @C3',N'@TenantId int,@C1 int,@C2 nvarchar(4000),@C3 datetime',@TenantId=1,@C1=1,@C2=N'Foo',@C3='2018-01-31 18:58:56.823'
</pre>
<p>Un saludo!</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com1tag:blogger.com,1999:blog-4364815135056214516.post-63940745767086232632017-12-13T00:09:00.001-08:002018-02-13T23:02:13.179-08:00Gestión de errores en SQL Server<p>Si te toca escribir “algo” de lógica de negocio en T-SQL, y te agobia la gestión de errores (como a mi), espero que después de este post tengamos los 2 las cosas un poco más claras.</p> <p>La primera opción que para tratar errores es con el estilo <i>old-school</i>, es decir, con <a href="https://docs.microsoft.com/en-us/sql/t-sql/functions/error-transact-sql">@@ERROR</a>, que devuelve un número de error si la última sentencia T-SQL ejecutada dio algún error, devolverá 0 si no hubo ningún error.</p> <p>Para todos los ejemplos vamos a usar una tabla con una sola columna.</p> <pre class="brush:sql;"> CREATE TABLE Table1 (Id INT PRIMARY KEY)
</pre>
<p>Usando @@ERROR</p>
<pre class="brush:sql;"> DELETE FROM Table1;
GO
INSERT INTO Table1 VALUES (1);
INSERT INTO Table1 VALUES (1);
IF @@ERROR <> 0
PRINT 'There was an error';
INSERT INTO Table1 VALUES (2);
GO
SELECT COUNT(*) FROM Table1;
</pre>
<p>Lo más relevante de este código es que, finalmente, la tabla tiene 2 registros, es decir, a pesar del error de la línea 4,
el resto del script se ha seguido ejecutando. Este comportamiento de seguir ejecutando el script es el predeterminado,
pero ¿qué pasa si no quiero que sea así? Pues podemos usar
<a href="https://docs.microsoft.com/en-us/sql/t-sql/statements/set-xact-abort-transact-sql">XACT_ABORT</a>, además
<a href="http://weblogs.sqlteam.com/dang/archive/2007/10/20/Use-Caution-with-Explicit-Transactions-in-Stored-Procedures.aspx">@@ERROR no parece una técnica muy segura</a>.</p>
<p>Si XACT_ABORT es ON, en caso de haber un error, se acaba la ejecución inmediatamente del lote y se revierte, si la hubiera,
la transacción explícita. Si es OFF, el valor predeterminado, pues funciona como el anterior script, la ejecución sigue
y no se revierte automáticamente ninguna transacción explícita (sólo la implícita que es la propia sentencia).</p>
<pre class="brush:sql;"> SET XACT_ABORT ON;
DELETE FROM Table1;
GO
INSERT INTO Table1 VALUES (1);
INSERT INTO Table1 VALUES (1);
INSERT INTO Table1 VALUES (2);
GO
SELECT COUNT(*) FROM Table1;
</pre>
<p>Es decir, la instrucción 6 no se ejecuta porque se aborta la ejecución del lote, por eso finalmente, sólo hay 1 registro
en la tabla destino.</p>
<p>Antes de ver como XACT_ABORT ON revierte automáticamente una transacción explícita, es importante conocer la función
<a href="https://docs.microsoft.com/en-us/sql/t-sql/functions/xact-state-transact-sql">XACT_STATE</a>. Esta función nos devuelve un valor que indica si hay o no una transacción explícita y en que estado está.</p>
<ul>
<li>1. Hay transacción.</li>
<li>0. No hay transacción.</li>
<li>-1. Hay transacción, pero un error hizo que la transacción no se pueda confirmar. La única operación válida es deshacer
toda la transacción.</li>
</ul>
<p>Las diferencias entre
<a href="https://docs.microsoft.com/en-us/sql/t-sql/functions/trancount-transact-sql">@@TRANCOUNT</a> y XACT_STATE es que @@TRANCOUNT permite saber si hay transacciones anidadas y XACT_STATE permite saber
si la transacción es
<i>confirmable</i>.</p>
<pre class="brush:sql;"> SET XACT_ABORT ON;
GO
DELETE FROM Table1;
GO
BEGIN TRAN;
INSERT INTO Table1 VALUES (1);
INSERT INTO Table1 VALUES (1);
INSERT INTO Table1 VALUES (2);
COMMIT TRAN;
GO
PRINT XACT_STATE();
PRINT @@TRANCOUNT;
IF XACT_STATE() = 1
COMMIT TRAN;
IF XACT_STATE() = -1
ROLLBACK TRAN;
GO
SELECT * FROM Table1;
</pre>
<p>Como la línea 7 da un error y XACT_ABORT es ON, pasa lo siguiente:</p>
<ul>
<li>Se aborta la ejecución del lote, no se ejecuta la línea 8.</li>
<li>Automáticamente se revierte la transacción explícita. Luego XACT_STATE y @@TRANCOUNT pasan a valer 0.</li>
</ul>
<p>Si comentáramos la línea 7, XACT_STATE valdría 1 y se ejecutaría COMMIT TRAN.</p>
<p>La línea 16 hace ROLLBACK TRAN si por algún motivo la transacción se volvió no confirmable.</p>
<p>De nuevo, antes de seguir es necesario entender otro concepto, como maneja los timeouts de cliente SQL Server. Un timeout
de cliente es como si pulsáramos “Cancel Executing Query” en SSMS, el botón Stop, vamos. Por ejemplo, creamos un procedimiento
almacenado como el siguiente:</p>
<pre class="brush:sql;"> CREATE PROCEDURE Foo
AS
PRINT 'Sergio';
WAITFOR DELAY '00:00:10';
PRINT 'panicoenlaxbox';
END
</pre>
<p>Si lo ejecutamos y antes de que pasen 10 segundos pulsamos Stop, sólo veremos la salida
<i>'Sergio'</i>, es decir, se deja de ejecutar el script y no vemos
<i>'panicoenlaxbox'</i>.</p>
<p>¿Desde una aplicación cliente funcionará igual?</p>
<p>Lo primero es poder ver PRINT en el SQL Server Profiler, esto no es necesario para comprobar esto, pero me parece útil poder
ver PRINT en el Profiler, lo he sacado de
<a href="http://www.annlewkowicz.com/2011/03/towards-print-statement-that-appears-in.html">aquí</a>. Creamos el procedimiento almacenado que hace la magia:</p>
<pre class="brush:sql;"> CREATE PROCEDURE PrintTrace1
@Text nvarchar(max)
AS
BEGIN
DECLARE @UserData BINARY(8000) = 0
DECLARE @UserInfo NVARCHAR(256) = SUBSTRING(@Text,1,256)
PRINT @Text
EXEC sp_trace_generateevent 82, @UserInfo, @UserData
END
</pre>
<p>Y modificamos el anterior procedimiento para lo use:</p>
<pre class="brush:sql;"> ALTER PROCEDURE Foo
AS
EXEC PrintTrace1 'Sergio';
WAITFOR DELAY '00:00:10';
EXEC PrintTrace1 'panicoenlaxbox';
</pre>
<p>Por último, cuando abramos el Profiler será necesario marcar el evento
<i>UserConfigurable:0</i> para ver la salida de PrintTrace1.</p>
<p>Ahora nuestro código cliente:</p>
<pre class="brush:csharp;"> using System;
using System.Data;
using System.Data.SqlClient;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
try
{
using (var connection = new SqlConnection(@"Server=(LocalDB)\MSSQLLocalDB;Database=Sergio;Trusted_Connection=True;"))
{
connection.Open();
using (var command = connection.CreateCommand())
{
command.CommandTimeout = 5;
command.CommandType = CommandType.StoredProcedure;
command.CommandText = "Foo";
command.ExecuteNonQuery();
}
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.ReadKey();
}
}
}
</pre>
<p>Con CommandTimeout 5 y WAITFOR DELAY '00:00:10' el timeout está garantizado, devolviendo el error típico al cliente
<i>Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding, </i>y
confirmando que igualmente deja de ejecutar el resto de script en el servidor:</p>
<p>
<a href="https://lh3.googleusercontent.com/-27NYBBZ8AIY/WjDgOC0PdeI/AAAAAAAAFWc/eqlPgkyZFlYfzd7vhDo_NOxmlpDWdXdIwCHMYCw/s1600-h/clip_image001%255B4%255D%255B2%255D">
<img width="244" height="40" title="clip_image001[4]" style="display: inline; background-image: none;" alt="clip_image001[4]" src="https://lh3.googleusercontent.com/-ac-Ap-5P5To/WjDgOu8FGVI/AAAAAAAAFWg/wK-imzm8eNQr13IyJnrnM_4hjbGlIzF9QCHMYCw/clip_image001%255B4%255D_thumb?imgmax=800" border="0">
</a>
</p>
<p>¿Y todo esto del timeout por qué? Pues porque ahora vamos a ver
<a href="https://docs.microsoft.com/en-us/sql/t-sql/language-elements/try-catch-transact-sql">TRY…CATCH</a>
de SQL Server, manejado estructurado de errores, buena cosa, pero era necesario tener los anteriores conceptos claro para
poder hablar sobre ellos.</p>
<p>De TRY…CATCH el ejemplo típico es el siguiente, donde al igual que pasaba con XACT_ABORT ON, ahora cuando se sucede un error
dentro del bloque TRY, la ejecución no continua, sino que salta al bloque CATCH.</p>
<pre class="brush:sql;"> DELETE FROM Table1;
GO
BEGIN TRY
BEGIN TRAN;
INSERT INTO Table1 VALUES (1);
-- RAISERROR with severity 11-19 will cause execution to jump to the CATCH block.
--RAISERROR ('Error raised in TRY block.', -- Message text.
-- 16, -- Severity.
-- 1 -- State.
-- );
INSERT INTO Table1 VALUES (1);
INSERT INTO Table1 VALUES (2);
COMMIT TRAN;
END TRY
BEGIN CATCH
SELECT
ERROR_NUMBER() AS ErrorNumber
,ERROR_SEVERITY() AS ErrorSeverity
,ERROR_STATE() AS ErrorState
,ERROR_PROCEDURE() AS ErrorProcedure
,ERROR_LINE() AS ErrorLine
,ERROR_MESSAGE() AS ErrorMessage;
IF XACT_STATE() <> 0
BEGIN
PRINT 'ROLLBACK TRAN';
ROLLBACK TRAN;
END
END CATCH
GO
SELECT * FROM Table1;
</pre>
<p>¿Cómo se llevará TRY…CATCH con un timeout?</p>
<p>Si hay un error de timeout no se ejecutará el CATCH, es decir, no creas que siempre que si hay un TRY…CATCH el CATCH siempre
está asegurado.</p>
<p>Modificando el procedimiento anterior y cancelando la ejecución desde SSMS vemos este comportamiento.</p>
<pre class="brush:sql;"> ALTER PROCEDURE Foo
AS
DELETE FROM Table1;
BEGIN TRY
BEGIN TRAN;
INSERT INTO Table1 VALUES (1);
EXEC PrintTrace1 'waitfor...';
WAITFOR DELAY '00:00:10';
EXEC PrintTrace1 'continue...';
INSERT INTO Table1 VALUES (1);
INSERT INTO Table1 VALUES (2);
COMMIT TRAN;
END TRY
BEGIN CATCH
EXEC PrintTrace1 'catch...';
IF XACT_STATE() <> 0
BEGIN
PRINT 'ROLLBACK TRAN';
ROLLBACK TRAN;
END
END CATCH
</pre>
<p>Y ahora la pregunta es: Si no puedo garantizar la ejecución del bloque CATCH, ¿debería activar siempre XACT_ABORT para garantizar
que la transacción explícita siempre se rechazara automáticamente? Pues parece que sí, porque quien
<a href="http://weblogs.sqlteam.com/dang/archive/2007/10/20/Use-Caution-with-Explicit-Transactions-in-Stored-Procedures.aspx#40919">defiende XACT_ABORT
</a>
lo hace porque si no está activo, un timeout de cliente podría dejar la conexión con recursos bloqueados hasta que la transacción
se cancele o la conexión se cierre, y asumiendo que hay pool de conexiones, un mal código de cliente podría arruinar
el servidor, incluso en un
<a href="http://weblogs.sqlteam.com/dang/archive/2007/10/20/Use-Caution-with-Explicit-Transactions-in-Stored-Procedures.aspx#40919">comentario</a>
del mismo post queda clara la jugada.</p>
<p>Por otro lado, si activamos XACT_ABORT y además hay un TRY…CATCH, el CATCH seguirá ejecutándose, pero XACT_STATE valdrá -1
(la única operación válida es deshacer la transacción) y además @@TRANCOUNT seguirá valiendo lo que valía, es decir,
un -1 en XACT_STATE no rechaza automáticamente la transacción explícita.</p>
<p>Y antes de llegar a nuestro snippet definitivo para la gestión de errores, hablemos de transacciones anidadas en SQL Server.
Poder se puede:</p>
<pre class="brush:sql;"> BEGIN TRAN
BEGIN TRAN
PRINT @@TRANCOUNT --2
COMMIT TRAN
PRINT @@TRANCOUNT --1
COMMIT TRAN
PRINT @@TRANCOUNT --0
</pre>
<p>Además, aparece el concepto de salvar una transacción que lo que permite es deshacer partes concretas de una transacción. Cabe mencionar que SAVE TRAN no incrementa @@TRANCOUNT y, por ende, ROLLBACK TRAN <em><nombre></em> tampoco
lo decrementa.</p>
<pre class="brush:sql;"> BEGIN TRAN
BEGIN TRAN
PRINT @@TRANCOUNT --2
SAVE TRAN st1
--Do something that can be rolled back
PRINT @@TRANCOUNT --2
ROLLBACK TRAN st1
COMMIT TRAN
PRINT @@TRANCOUNT --1
COMMIT TRAN
PRINT @@TRANCOUNT --0
</pre>
<p>Un ROLLBACK TRAN (sin nombre) deshace todas las transacciones (anidadas también si las hubiera) y decrementa @@TRANCOUNT
a 0. ROLLBACK TRAN <em><nombre></em> sólo es válido si <em><nombre></em> es un SAVE TRAN o un BEGIN TRAN <em><nombre></em> siendo
esa transacción la más externa (no siendo anidada).</p>
<p>Un COMMIT TRAN (sin nombre) confirma la transacción actual según su nivel de indentación, aunque es válido un COMMIT TRAN
<em> <nombre></em> refiriéndose tanto a una transacción anidada como a una externa.</p>
<p>Como resumen, con transacciones anidadas podemos o bien rechazar todas las transacciones (ROLLBACK TRAN o ROLLBACK TRAN <em><nombre_de_la_más_externa></em>)
o bien rechazar partes de una transacción anidada (SAVE TRAN <em><nombre></em> y ROLLBACK <em><nombre></em>).</p>
<p>Y en este momento, es cuando vemos la plantilla de un procedimiento almacenado que he sacado de este
<a href="http://rusanu.com/2009/06/11/exception-handling-and-nested-transactions/">post</a>
donde le agregamos XACT_ABORT ON para que un timeout de cliente no nos de guerra.</p>
<pre class="brush:sql;"> CREATE PROCEDURE [ProcedureName]
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @trancount INT;
SET @trancount = @@TRANCOUNT;
BEGIN TRY
IF @trancount = 0
BEGIN TRANSACTION;
ELSE
SAVE TRANSACTION ProcedureName;
-- Do something...
IF @trancount = 0
COMMIT;
END TRY
BEGIN CATCH
DECLARE @errorNumber INT, @message NVARCHAR(4000), @xact_state INT;
SELECT @errorNumber = ERROR_NUMBER(), @message = ERROR_MESSAGE(), @xact_state = XACT_STATE();
IF @xact_state = -1
ROLLBACK;
IF @xact_state = 1 AND @trancount = 0
ROLLBACK;
IF @xact_state = 1 AND @trancount > 0
ROLLBACK TRANSACTION ProcedureName;
RAISERROR('ProcedureName: %d: %s', 16, 1, @errorNumber, @message) ;
END CATCH
END
</pre>
<p>Ahora sí, podemos escribir un “poco” de lógica de negocio en T-SQL con una estrategia clara de gestión de errores.</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com3tag:blogger.com,1999:blog-4364815135056214516.post-36061615875641985312017-11-03T07:29:00.008-07:002018-08-31T10:36:52.948-07:00Crear e inicializar un contexto en EF Core<p>En EF 6.x, crear e inicializar un contexto tiene <a href="http://panicoenlaxbox.blogspot.com.es/2013/04/romper-el-hielo-con-ef-code-first.html">magia</a>. Magia en el sentido de que Entity Framework puede decidir automáticamente dónde y con que nombre crear la base de datos. Según el constructor elegido de DbContext, si existe o no una cadena de conexión o un inicializador en un fichero .config, incluso saber que prevalece lo escrito en un fichero .config sobre la configuración por código… en fin, magia, para lo bueno y para lo malo. </p> <p>En EF Core (actualmente 2.0) se ha prescindido de esa magia, ahora tenemos que ser explícitos sobre la configuración del contexto. Esto parece una buena idea, no creo que haya mucho debate, pero por el contrario arrancar un proyecto requiere saber más sobre como inicializar y trabajar con EF.</p> <p>Asumiendo que estamos trabajando con una aplicación de consola, la forma recomendada de agregar EF Core a un proyecto es instalar un proveedor.</p> <pre class="brush:csharp">dotnet add package Microsoft.EntityFrameworkCore.SqlServer
</pre>
<p>Después, habrá que agregar el CLI de EF Core y/o los comandos de PowerShell para el PMC (Power Management Console).</p>
<p>Para agregar el CLI de EF Core hay que editar el .csproj a mano.</p>
<pre class="brush:xml;"><ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
</ItemGroup>
</pre>
<p>Para agregar los comandos al PMC, podemos hacerlo agregando un paquete con normalidad.</p>
<pre class="brush:csharp">dotnet add package Microsoft.EntityFrameworkCore.Tools
</pre>
<p>Nuestro .csproj quedaría así:</p>
<pre class="brush:xml;"><Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
</ItemGroup>
</Project>
</pre>
Si estamos en una aplicación de consola también será necesario agregar una referencia a Microsoft.EntityFrameworkCore.Design<p>Ahora ya estamos preparados para trabajar con EF Core, así que creamos un contexto cualquiera:</p>
<pre class="brush:csharp">class ShopContext : DbContext
{
public DbSet<Order> Orders { get; set; }
}
internal class Order
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
}
</pre>
<p>Y al crear nuestra primera migración, la primera en la frente:</p>
<pre class="brush:csharp">dotnet ef migrations add Initial</pre>
<p><em>No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring
method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your
DbContext type accepts a DbContextOptions
object in its constructor and passes it to the base constructor for DbContext.</em>
</p>
<p>El error nos está diciendo que el contexto no está “configurado” con ningún proveedor de base de datos, que no sabe a que bd atacar… que o bien usemos DbContext.OnConfiguring o bien AddDbContext
sobre el “proveedor de servicios de la aplicación (el contenedor de dependencias)” y que si usamos este último método
no olvidemos crear un constructor que acepte DbContextOptions
<tcontext> y que llame a la base.</tcontext>
</p>
<p>La primera solución pasa por sobrescribir OnConfiguring</p>
<pre class="brush:csharp">protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer(@"Server=(LocalDB)\MSSQLLocalDB;Database=Example;Trusted_Connection=True;");
}
base.OnConfiguring(optionsBuilder);
}
</pre>
<p>“IsConfigured” lo que viene a decir es “si no me han configurado ya, entonces me configuro”, luego veremos que será posible
configurar el contexto desde fuera, porque si no lo hiciéramos y quisiéramos cambiar de proveedor o de cadena de conexión
tendríamos que meter más código aquí, pero, en cualquier caso, esté o no configurado el contexto está bien saber que
tenemos una última oportunidad para hacer lo que queramos con la configuración. Además, hay que tener en cuenta que OnConfiguring
se llama siempre para cada instancia creada del contexto.</p>
<p>Ahora ya funciona crear una migración con las herramientas cliente y además podríamos usar el contexto en nuestro código:</p>
<pre class="brush:csharp">
using (var context = new ShopContext())
{
}
</pre>
<p>Sin embargo, parece mejor que el contexto sea configurado desde fuera, será más versátil y no habremos hardcodeado nada,
probemos la segunda opción.</p>
<pre class="brush:csharp">using System;
using Microsoft.EntityFrameworkCore;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
var options = new DbContextOptionsBuilder()
.UseSqlServer(@"Server=(LocalDB)\MSSQLLocalDB;Database=Example;Trusted_Connection=True;")
.Options;
using (var context = new ShopContext(options))
{
context.Database.Migrate();
}
}
}
class ShopContext : DbContext
{
public ShopContext(DbContextOptions options) : base(options)
{
}
public DbSet<Order> Orders { get; set; }
}
internal class Order
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
}
}
</pre>
<p>Ahora somos capaces de inyectar la configuración al contexto (luego IsConfigured vale true en OnConfiguring).</p>
<p>Tanto DbContextOptions como DbContextOptionsBuilder tiene su versión genérica, que cuando trabajemos más adelante con DI
nos servirá si tenemos más de un contexto en nuestra aplicación.</p>
<pre class="brush:csharp">
var options = new DbContextOptionsBuilder<ShopContext>()
.UseSqlServer(@"Server=(LocalDB)\MSSQLLocalDB;Database=Example;Trusted_Connection=True;")
.Options;
public ShopContext(DbContextOptions<ShopContext> options) : base(options)
{
}
</pre>
<p>Sin embargo, lo que deja de funcionar ahora es el CLI, porque, aunque encuentra el contexto, no tiene un constructor sin
parámetros y por ende no se puede instanciar.</p>
<p><em>Unable to create an object of type 'ShopContext'. Add an implementation of 'IDesignTimeDbContextFactory
' to the project, or see https://go.microsoft.com/fwlink/?linkid=851728 for additional patterns supported at design time.</em>
</p>
<p>
<a href="https://docs.microsoft.com/en-us/ef/core/miscellaneous/configuring-dbcontext#using-idesigntimedbcontextfactorytcontext">IDesignTimeDbContextFactory
<tcontext></tcontext>
</a> ayuda a las herramientas de cliente de EF a crear un contexto si el mismo no tiene un constructor público sin parámetros.</p>
<pre class="brush:csharp">
class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<ShopContext>
{
public ShopContext CreateDbContext(string[] args)
{
var options = new DbContextOptionsBuilder<ShopContext>()
.UseSqlServer(@"Server=(LocalDB)\MSSQLLocalDB;Database=Example;Trusted_Connection=True;")
.Options;
return new ShopContext(options);
}
}
</pre>
<p>Esta clase sólo será usada por las herramientas cliente, esto es importante, es tiempo de diseño, nada tiene que ver con
tiempo de ejecución.</p>
<p>Ahora ya funcionan las migraciones.</p>
<p>Aunque con lo visto hasta aquí se podría funcionar, no estamos haciendo uso del contenedor de dependencias.</p>
<p>Para hacerlo en una aplicación de consola.</p>
<pre class="brush:csharp">
static void Main(string[] args)
{
var options = new DbContextOptionsBuilder()
.UseSqlServer(@"Server=(LocalDB)\MSSQLLocalDB;Database=Example;Trusted_Connection=True;")
.Options;
IServiceCollection services = new ServiceCollection();
services
.AddSingleton(options)
.AddScoped<ShopContext>();
ServiceProvider serviceProvider = services.BuildServiceProvider();
using (var context = serviceProvider.GetService<ShopContext>())
{
context.Database.EnsureCreated();
}
}
</pre>
<p>Y si queremos simplificar el registro de servicios, tenemos el método de extensión AddDbContext, que registra el contexto
como Scoped y nos da una lamba que se llamará la primera vez que se resuelva el contexto y que devolverá un DbContextOptions,
que se registrará como Singleton.</p>
<pre class="brush:csharp">
static void Main(string[] args)
{
IServiceCollection services = new ServiceCollection();
services.AddDbContext<ShopContext>(builder =>
{
builder.UseSqlServer(@"Server=(LocalDB)\MSSQLLocalDB;Database=Example;Trusted_Connection=True;");
});
ServiceProvider serviceProvider = services.BuildServiceProvider();
using (var context = serviceProvider.GetService<ShopContext>())
{
context.Database.EnsureCreated();
}
}
</pre>
<p>En realidad, AddDbContext registra muchos más servicios con su llamada a
<a href="https://github.com/aspnet/EntityFrameworkCore/blob/e76f8f3d179ff274e5f2ac4c716b206880ab1781/src/EFCore/EntityFrameworkServiceCollectionExtensions.cs#L281">AddCoreServices</a>
</p>
<p>Si por el contrario nuestra aplicación es web la cosa cambia.</p>
<p>De serie (y asumiendo que estamos usando Visual Studio) viene con el paquete <strike>Microsoft.AspNetCore.All</strike> Microsoft.AspNetCore.App que ya incluye los
paquetes Microsoft.EntityFrameworkCore.SqlServer, Microsoft.EntityFrameworkCore.Tools y Microsoft.EntityFrameworkCore.Design, <strike>es decir, para tener lo mismo
que en el ejemplo anterior sólo tendríamos que agregar el CLI de EF (el de PMC ya viene de serie)</strike> y también el CLI de EF (en ASP.NET Core 2.1, dotnet ef es un comando de serie)</p>
<p>Además, ahora estas herramientas encontrarán el método
<a href="https://docs.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/#update-main-method-in-programcs">BuildWebHost</a>
</p>
<pre class="brush:csharp">
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
</pre>
<p>Es importante saber que se busca el método BuildWebHost con ese nombre exacto, cualquier otro nombre no valdrá.</p>
<p>Si las herramientas cliente encuentran BuildWebHost lo ejecutarán y también ejecutarán Startup.Configure y Startup.ConfigureServices
(porque aquí es donde habremos llamado a AddDbContext y registrado los servicios).</p>
<p>La consecuencia directa de esto es que el código de inicialización (migración, seed, etc) que tuviéramos en Startup.Configure
ya no debería estar allí. Se recomienda moverlo al método Main.</p>
<pre class="brush:csharp">public static void Main(string[] args)
{
// Construir IWebHost
var host = BuildWebHost(args);
// Inicialización
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<ShopContext>();
// Hacer algo útil...
}
catch (Exception ex)
{
ILogger<Program> logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
}
}
// Ejecutar IWebHost
host.Run();
}
</pre>
<p>En este
<a href="https://gist.github.com/unaizorrilla/17ee153933ef253a22b6a7f6a744e423">gist</a> de Unai Zorrila hay un ejemplo más molón</p>
<p>Como dato curioso (o no tan curioso y sí peligroso), si pasara que tenemos tanto BuildWebHost como IDesignTimeDbContextFactory,
se ejecutarían ambos, aunque prevalecería IDesignTimeDbContextFactory.</p>
<p>Y para terminar, además del método extensor AddDbContext, en EF Core 2 han metido el nuevo método de extensión
<a href="https://docs.microsoft.com/en-us/ef/core/what-is-new/#high-performance">AddDbContextPool</a> que usa un pool de contextos para mejorar el rendimiento, aunque tiene ciertas limitaciones.</p>
<p>¡Un saludo!</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com0tag:blogger.com,1999:blog-4364815135056214516.post-20732154700786585582017-09-19T01:54:00.001-07:002017-09-19T01:54:35.703-07:00Merge vs Rebase<p>Cuando se trabaja en equipo todo el mundo acepta con naturalidad la necesidad de un coding standard o similar para buscar
la mayor legibilidad, consistencia y mantenibilidad del código. Igualmente, y en relación al control de código fuente,
debería haber un consenso sobre que flujo de trabajo y tipo de estrategia de merge seguir.</p>
<p>Trabajando con git, tarde o temprano hay que elegir, merge o rebase, o para ser más exactos, estrategia de merge de tipo
fast-forward o tipo recursive.</p>
<p>Cualquier opción es válida y ambas tienen ventajas y desventajas.</p>
<p>En mi caso, intentaré explicar que es merge y rebase.</p>
<strong>TL;DR</strong>
<blockquote>
<p>
<i>Merge con estrategia recursive hace explícita la integración de ramas y además mantiene contiguos todos los commits de una característica, por el contrario, dificulta la legibilidad del repositorio y podemos acabar fácilmente con un guitar hero <a href="http://devhumor.com/media/i-fucked-up-git-so-bad-it-turned-into-guitar-hero">http://devhumor.com/media/i-fucked-up-git-so-bad-it-turned-into-guitar-hero</a> </i></p>
<p>
<i>Por otro lado, rebase mantiene una historia lineal del proyecto, pero está sujeto a más situaciones comprometidas, es más propenso a meter la pata. La regla de oro es no hacer rebase sobre commits públicos, o al menos no hacerlo sobre commits sobre los que cualquier miembro del equipo haya basado su trabajo. Además, con rebase perdemos la trazabilidad de cuando se integró una rama.
</i>
</p>
<p>
<i>En mi caso apuesto por merge, forzando incluso con --no-ff para evitar una estrategia fast-forward y así ser totalmente explícito de cuando se integró una rama. Sin embargo, para descargar cambios del repositorio remoto me parece adecuado usar git pull --rebase para evitar ciertos commits extra de merge que no aportan valor al repositorio.</i></p>
<p>
<i>Por último, el rebase de tipo clean-up es gratis en local y parece una buena práctica ¿Por qué no usarlo?</i>
</p>
</blockquote>
<p>Para los ejemplos, asumo que no tienes guardado nada de valor en el directorio C:\Temp y deberían poderse seguir de principio
a final si no te cambias de directorio. Además, y aunque normalmente los ejemplos se ejecutan por comandos, también mencionaré
de vez en cuando a SourceTree porque es el cliente de git que uso habitualmente.</p>
<p>Para hacer nuestro primer merge:</p>
<pre class="brush: bash;">mkdir C:\Temp
cd C:\Temp
mkdir example
cd example
echo F1 > F1.txt
git init
git add .
git commit -m "C1"
echo F2 > F2.txt
git add .
git commit -m "C2"
git checkout -b develop
echo F3 > F3.txt
git add .
git commit -m "C3"
git checkout master
echo F4 > F4.txt
git add .
git commit -m "C4"
</pre>
<p>
Aquí la historia se ha bifurcado porque se han hecho commits en distintas ramas desde un mismo commit base.</p>
<a href="https://lh3.googleusercontent.com/-3nPqN6zOL-E/WcDawaB56nI/AAAAAAAAFFY/qT689p452gUpwprmEToJzmDL_F0JDRXmwCHMYCw/s1600-h/clip_image001%255B3%255D"><img width="244" height="96" title="clip_image001" style="margin: 0px; display: inline; background-image: none;" alt="clip_image001" src="https://lh3.googleusercontent.com/-y8yxiwSYZM4/WcDaw3K-0oI/AAAAAAAAFFc/K-NCMrQF9L4BJTZKwJMC14ix4LU7orG2QCHMYCw/clip_image001_thumb?imgmax=800" border="0"></a>
<p>Para integrar los cambios de develop en master tenemos 2 opciones: merge o rebase.</p>
<p>Los mantras oficiales (grábatelos a fuego) son:</p>
<ul>
<li>merge “me voy a y cojo de”</li>
<li>rebase “estoy en y rebaso a”</li>
</ul>
<p>Por ahora veremos merge y más adelante rebase</p>
<pre class="brush: bash;">git checkout master
git merge develop
</pre>
<a href="https://lh3.googleusercontent.com/-BfmRdy4bWo8/WcDaxXsR_gI/AAAAAAAAFFg/kfe5bhlp0EwtnGaZ_K9VtYjfw9T6JfkiACHMYCw/s1600-h/clip_image002%255B3%255D"><img width="244" height="82" title="clip_image002" style="margin: 0px; display: inline; background-image: none;" alt="clip_image002" src="https://lh3.googleusercontent.com/-q84VrfnomyM/WcDaxxKMbZI/AAAAAAAAFFk/6ISBQudK39k6RDKrM6e7yWER2ADOp2U9ACHMYCw/clip_image002_thumb?imgmax=800" border="0"></a>
<p>Ahora el commit de merge tiene 2 padres. El primero es el commit de la rama donde se estaba (master) y el segundo es el commit
de la rama que se integró (C3 de develop). El orden es importante porque después (y si fuera necesario) podríamos ver
sólo los commits de una rama (excluyendo los que vinieron por integración de otras ramas) con git log --oneline --first-parent.
Otra opción muy socorrida es git log --oneline --no-merge que muestra el log sin ningún commit de merge.</p>
<p>Para probar otro tipo de merge, tenemos que volver a la situación anterior, como si no hubiéramos hecho el merge. Para ello
bastaría con retroceder la rama actual un commit con git reset --hard master^, el problema es que en Windows hay que
poner dos acentos circunflejos porque hay que escapar ciertos caracteres <a href="https://stackoverflow.com/questions/9600549/using-a-caret-when-using-git-for-windows-in-powershell">https://stackoverflow.com/questions/9600549/using-a-caret-when-using-git-for-windows-in-powershell</a>,
así que en vez de poner git reset --hard master^ será mejor poner git reset --hard master~ que para el caso es lo mismo.</p>
<pre class="brush: bash;">git reset --hard master~
git checkout develop
git merge master
</pre>
<a href="https://lh3.googleusercontent.com/-lv-BoFVQJTw/WcDayIRUUvI/AAAAAAAAFFo/oWB_r4_a4usEOwj8zVF5CimtbZupJc2UACHMYCw/s1600-h/clip_image004%255B3%255D"><img width="244" height="65" title="clip_image004" style="margin: 0px; display: inline; background-image: none;" alt="clip_image004" src="https://lh3.googleusercontent.com/-vkT4BHkxNaI/WcDayuIFVoI/AAAAAAAAFFs/qMaJemttOj8Ci9ve0agsNZQTYzQPgZf7wCHMYCw/clip_image004_thumb?imgmax=800" border="0"></a>
<p>Como vemos, el cambio más reseñable es que el mensaje por defecto del commit de merge es distinto.</p>
<ul>
<li>Merge branch ‘<rama-mergeada>’ si se está integrando en master</li>
<li>Merge branch ‘<rama-mergeada>’ into <rama> si se está integrando en cualquier otra rama</li>
</ul>
<p>Si estamos trabajando solos, esos serían los dos tipos de commits de merge que deberíamos encontrarnos normalmente.</p>
<p>Podría haber un tercero, si un merge que podría haber sido resuelto con la estrategia “fast-forward” (esto es, simplemente
avanzar el puntero sin crear ningún commit de merge) lo forzamos para que sí haya un commit de merge con --no-ff</p>
<pre class="brush: bash;">cd c:\Temp
rm -rf *
mkdir example
cd example
echo F1 > F1.txt
git init
git add .
git commit -m "C1"
echo F2 > F2.txt
git add .
git commit -m "C2"
git checkout -b develop
echo F3 > F3.txt
git add .
git commit -m "C3"
git checkout master
</pre>
<a href="https://lh3.googleusercontent.com/-qkyi9e7s6EE/WcDazCeiP8I/AAAAAAAAFFw/_5z6UdlVXLETTkNS3QXn453Z8_UiBn22ACHMYCw/s1600-h/clip_image005%255B3%255D"><img width="244" height="78" title="clip_image005" style="margin: 0px; display: inline; background-image: none;" alt="clip_image005" src="https://lh3.googleusercontent.com/-VVB0xTIVmGE/WcDazgROj3I/AAAAAAAAFF0/k5DmEk0Zi8IoCvnT_EaZfyAItBGzvwJJgCHMYCw/clip_image005_thumb?imgmax=800" border="0"></a>
<pre class="brush: bash;">git merge develop
</pre><p>
<a href="https://lh3.googleusercontent.com/-gbwOqIjS1bM/WcDa0EIQgJI/AAAAAAAAFF4/6qs001XZV8M2gov4HDEQ-PgKrl1aoWjAwCHMYCw/s1600-h/clip_image006%255B3%255D"><img width="244" height="78" title="clip_image006" style="margin: 0px; display: inline; background-image: none;" alt="clip_image006" src="https://lh3.googleusercontent.com/-s-m7XpGt-4E/WcDa0ttOoYI/AAAAAAAAFF8/2W-_DkPerkUvvvhK_-4RuQZjDBe7acFOwCHMYCw/clip_image006_thumb?imgmax=800" border="0"></a>
</p><p>
<a href="https://lh3.googleusercontent.com/-Kr2lNyM2LYk/WcDa1GwyLSI/AAAAAAAAFGA/6x2L-9UXJ4Ao-D40-PMMzaFF60ZUhwhYACHMYCw/s1600-h/clip_image007%255B3%255D"><img width="244" height="54" title="clip_image007" style="margin: 0px; display: inline; background-image: none;" alt="clip_image007" src="https://lh3.googleusercontent.com/-wk43fC1HOHg/WcDa1v8GDCI/AAAAAAAAFGE/XPuK2yvZI_MDjyw5PMSab9DPsfJyFUXBgCHMYCw/clip_image007_thumb?imgmax=800" border="0"></a>
</p><p>Si ahora hacemos esto otro</p>
<pre class="brush: bash;">git reset --hard master~
git merge develop --no-ff
</pre><p>
<a href="https://lh3.googleusercontent.com/-2wkvg7NW844/WcDa176JzMI/AAAAAAAAFGI/6gxSZJs61YknHVC-s96sScJuIkS-W-sjACHMYCw/s1600-h/clip_image008%255B3%255D"><img width="244" height="57" title="clip_image008" style="margin: 0px; display: inline; background-image: none;" alt="clip_image008" src="https://lh3.googleusercontent.com/-BnHRyTlD9k8/WcDa2fLdY2I/AAAAAAAAFGM/T3XV4fJfmhEIQO8sRoguTB7TCNFPZMjWwCHMYCw/clip_image008_thumb?imgmax=800" border="0"></a>
</p><p><a href="https://lh3.googleusercontent.com/-hob9KmHjHlA/WcDa20nieII/AAAAAAAAFGQ/kJFbXB1NMW0Ex5HwZmnwKJhHdgoFkZ1wQCHMYCw/s1600-h/clip_image009%255B3%255D"><img width="244" height="63" title="clip_image009" style="margin: 0px; display: inline; background-image: none;" alt="clip_image009" src="https://lh3.googleusercontent.com/-C-i6ky6m7GE/WcDa3UqLiuI/AAAAAAAAFGU/DZxtYHmeiHAwE5oYNC1WiJUSbxZDJxDsQCHMYCw/clip_image009_thumb?imgmax=800" border="0"></a>
</p><p>Está claro, si la estrategia de merge es recursive será cuando veamos un commit de merge, si es fast-forward no.</p>
<p>--no-ff desde Source Tree equivale a marcar la casilla “Create a new commit even if fast-forward is possible”</p>
<a href="https://lh3.googleusercontent.com/-KfXvpysxeZc/WcDa3jUD7DI/AAAAAAAAFGY/NmrXnZ5RStQ9dcqGlVYR-MnoFsJFS1GmgCHMYCw/s1600-h/clip_image010%255B3%255D"><img width="244" height="67" title="clip_image010" style="margin: 0px; display: inline; background-image: none;" alt="clip_image010" src="https://lh3.googleusercontent.com/-eLmBaIEUKzs/WcDa4CGtqBI/AAAAAAAAFGc/CxNS7WHTrs0pLpxjN_LdqNdix0QxKORYgCHMYCw/clip_image010_thumb?imgmax=800" border="0"></a>
<p>Sin embargo, si trabajando en equipo, pueden aparecer otros commits de merge cuando nos traigamos los cambios del remoto
con git pull</p>
<p>Para trabajar con un remoto y no tener que crearlo en github, bitbucket o similar, podemos usar un repositorio bare y así
todo queda en casa.</p>
<pre class="brush: bash;">cd C:\Temp
rm -rf *
mkdir central
cd central
git init --bare
cd ..
mkdir example1
cd example1
echo F1 > F1.txt
git init
git add .
git commit -m "C1"
echo F2 > F2.txt
git add .
git commit -m "C2"
git remote add origin C:\Temp\central
git push -u origin master
cd ..
git clone C:\Temp\central example2
</pre>
<p>En este momento ya tenemos un repositorio remoto y 2 repositorios locales, ¡todo listo!</p>
<pre class="brush: bash;">cd example1
echo F3 > F3.txt
git add .
git commit -m "C3"
git push
</pre>
<a href="https://lh3.googleusercontent.com/-fQ9FM35a7EM/WcDa4uR6dVI/AAAAAAAAFGg/BFpDMYDC9w07Fje5gFGf5AE54zrME0dVACHMYCw/s1600-h/clip_image011%255B3%255D"><img width="244" height="52" title="clip_image011" style="margin: 0px; display: inline; background-image: none;" alt="clip_image011" src="https://lh3.googleusercontent.com/-3fvRCUb36EE/WcDa4wEAIhI/AAAAAAAAFGk/7eJmUD0Kzn0T9ZogYuEyM0BvBSjN7QMvgCHMYCw/clip_image011_thumb?imgmax=800" border="0"></a>
<pre class="brush: bash;">cd ..
cd example2
echo F4 > F4.txt
git add .
git commit -m "C4"
</pre>
<a href="https://lh3.googleusercontent.com/-mY3wvymbdMA/WcDa5QjmU5I/AAAAAAAAFGo/x31NBteexqkYkuvwE9fAH51iJ07MkFzKgCHMYCw/s1600-h/clip_image012%255B3%255D"><img width="244" height="50" title="clip_image012" style="margin: 0px; display: inline; background-image: none;" alt="clip_image012" src="https://lh3.googleusercontent.com/-2clq0eNxlSo/WcDa56jNQHI/AAAAAAAAFGs/lK_DCZPYpcAjx8KcNkCcda3tl3gEFYKbgCHMYCw/clip_image012_thumb?imgmax=800" border="0"></a>
<p>Si hacemos un git push desde example2 fallará porque no estamos al día, hay cambios en el remoto que no nos hemos bajado.</p>
<pre class="brush: bash;">git fetch
</pre>
<a href="https://lh3.googleusercontent.com/-V3C5A8KRAK4/WcDa6GMYEJI/AAAAAAAAFGw/HUrOrL5Uz_IbXqU7jzg04tVa4EtmYyv6wCHMYCw/s1600-h/clip_image013%255B3%255D"><img width="244" height="59" title="clip_image013" style="margin: 0px; display: inline; background-image: none;" alt="clip_image013" src="https://lh3.googleusercontent.com/-0VaIzorYTUQ/WcDa6rtlWRI/AAAAAAAAFG0/AdjM8F_YgYIhej3P1YL4K7heNi0EJNrGQCHMYCw/clip_image013_thumb?imgmax=800" border="0"></a>
<p>Para ver git pull me parece interesante comentar la ventana de SourceTree</p>
<a href="https://lh3.googleusercontent.com/-GYHy0YBmyiA/WcDa7YQrWXI/AAAAAAAAFG4/evOeNOGm4dkg_swUU4ga0YY1hpSW_I1DACHMYCw/s1600-h/clip_image015%255B3%255D"><img width="244" height="92" title="clip_image015" style="margin: 0px; display: inline; background-image: none;" alt="clip_image015" src="https://lh3.googleusercontent.com/-Raxveg8lk9w/WcDa778k9YI/AAAAAAAAFG8/kK38XrYs1KQ8Y8iVSNc4XznfYjEIKa83QCHMYCw/clip_image015_thumb?imgmax=800" border="0"></a>
<p>Si no está marcado “Commit merged changes immediately”, sería un git pull --no-commit, luego tendríamos los cambios en el
working copy pero no se hará el commit.</p>
<a href="https://lh3.googleusercontent.com/-6IZhX6h_ZFk/WcDa8MhrA7I/AAAAAAAAFHA/7Yftua-HRbEH2IY5iwPcCkkrdiZZKD69QCHMYCw/s1600-h/clip_image016%255B3%255D"><img width="244" height="80" title="clip_image016" style="margin: 0px; display: inline; background-image: none;" alt="clip_image016" src="https://lh3.googleusercontent.com/-opiAprH0pkE/WcDa8tVsbAI/AAAAAAAAFHE/-_hVWoKCpbckHKo7aT3haofSe67BTDDkQCHMYCw/clip_image016_thumb?imgmax=800" border="0"></a>
<p>“Create a new commit even if fast-forward is possible” indicará si agregar o no el modificador --no-ff</p>
<p>“Rebase instead of merge (WARNING: make sure you haven’t pushed your changes)” ejecutará git pull --rebase</p>
<p>En caso de no estar marcada ninguna opción, simplemente será un git pull</p>
<a href="https://lh3.googleusercontent.com/-C6jYtp5ErTM/WcDa9AftHJI/AAAAAAAAFHI/3HwOQSNHOVowoKjxBi7xgFpFFrUPPO-CQCHMYCw/s1600-h/clip_image017%255B3%255D"><img width="244" height="56" title="clip_image017" style="margin: 0px; display: inline; background-image: none;" alt="clip_image017" src="https://lh3.googleusercontent.com/-MfU-zpW-tko/WcDa9Z2YXLI/AAAAAAAAFHM/VXBoWbLw7EAuvledvyKZ9McgeyThxrzNACHMYCw/clip_image017_thumb?imgmax=800" border="0"></a>
<p>La diferencia más notable en el commit de merge es que ahora el mensaje predeterminado es “Merge branch ‘<rama>’ of
‘dirección_remoto’</p>
<p>git pull es lo mismo que hacer git fetch + git merge origin/<current-branch>, la única diferencia sería el mensaje
predeterminado del commit de merge, que ahora sería “Merge remote-tracking branch 'origin/<rama>'”</p>
<a href="https://lh3.googleusercontent.com/-3-XAbmeNE8c/WcDa97FuNtI/AAAAAAAAFHQ/VZX9yHJ-NN464nZW0-88bC1jWBafjjnGwCHMYCw/s1600-h/clip_image018%255B5%255D"><img width="244" height="56" title="clip_image018" style="margin: 0px; display: inline; background-image: none;" alt="clip_image018" src="https://lh3.googleusercontent.com/-JY8ohQrOUPs/WcDa-d6fq7I/AAAAAAAAFHU/vLrAy-KnL5oU0uS_AB5Zqwi7WOnhnoy0ACHMYCw/clip_image018_thumb?imgmax=800" border="0"></a>
<p>Y si hubiéramos lanzado git pull --rebase, nuestro commit C4 se aplica en lo alto de la rama evitando un commit extra de
merge</p>
<a href="https://lh3.googleusercontent.com/-uV6Vp3Tq9VQ/WcDa-_czXJI/AAAAAAAAFHY/nVthw-egIZMLbLKY739mD5gDbI_HLNQ6wCHMYCw/s1600-h/clip_image019%255B5%255D"><img width="244" height="63" title="clip_image019" style="margin: 0px; display: inline; background-image: none;" alt="clip_image019" src="https://lh3.googleusercontent.com/-cR1NeRtgdEY/WcDa_Vy1VVI/AAAAAAAAFHc/P8gWE6OUE4UWzWzkuGV-wQDcYe0cvVZUACHMYCw/clip_image019_thumb?imgmax=800" border="0"></a>
<p>Y llegados a este punto ya tenemos claro de dónde vienen los distintos commits de merge, cuando se pueden producir y como
reconocerlos atendiendo a sus mensajes predeterminados.</p>
<p>Antes de meternos con rebase es importante entender el concepto de reescribir la historia y porqué tiene mucho peligro.</p>
<p>Seguramente haya muchos más comandos que reescriban la historia, pero los más habituales son git commit --amend y git rebase</p>
<pre class="brush: bash;">cd C:\Temp
rm -rf *
mkdir example
cd example
echo F1 > F1.txt
git init
git add .
git commit -m "C1"
echo F2 > F2.txt
git add .
git commit -m "C2"
</pre>
<a href="https://lh3.googleusercontent.com/-kL7SSXR7LO0/WcDa_4wGcEI/AAAAAAAAFHg/Ba2ZXzO28-gnGsgpA62sd3J6fXvtWxyCgCHMYCw/s1600-h/clip_image020%255B5%255D"><img width="244" height="48" title="clip_image020" style="margin: 0px; display: inline; background-image: none;" alt="clip_image020" src="https://lh3.googleusercontent.com/-qXBTltpGVGI/WcDbAGK8c3I/AAAAAAAAFHk/lUnLFkpapUs6tdqE51tKcjPiMhQss-L-QCHMYCw/clip_image020_thumb?imgmax=800" border="0"></a>
<p>Ahora nos damos cuenta de que en el commit C2 el texto del fichero F2.txt no es correcto o no nos gusta el mensaje del commit
o nos hemos dejado algún fichero en el index o sin guardar <a href="https://marketplace.visualstudio.com/items?itemName=PaulCBetts.SaveAllTheTime">https://marketplace.visualstudio.com/items?itemName=PaulCBetts.SaveAllTheTime</a> y queremos que sea parte del commit y, sea como sea, no queremos hacer un nuevo commit sino reemplazar el último. Esto
se consigue con --amend que combina el staging area/index con el contenido del último commit y reemplaza el último commit.
Con --no-edit simplemente decimos que nos vale el mensaje del último commit.</p>
<pre class="brush: bash;">echo F2_upated > F2.txt
git add .
git commit --amend --no-edit
</pre>
<p>El identificador del commit ha cambiado, hemos reescrito la historia.</p>
<a href="https://lh3.googleusercontent.com/-WfTq24sbJsQ/WcDbAgr9mjI/AAAAAAAAFHo/5kwEBa9DqkgW7855R4R2hb_FajRcoe7MACHMYCw/s1600-h/clip_image021%255B5%255D"><img width="244" height="55" title="clip_image021" style="margin: 0px; display: inline; background-image: none;" alt="clip_image021" src="https://lh3.googleusercontent.com/-sWuBNAtyWtM/WcDbBFhX4LI/AAAAAAAAFHs/levWuFQKfD4dNZzKIpdF6W18F8-VwGGswCHMYCw/clip_image021_thumb?imgmax=800" border="0"></a>
<p>En SourceTree también podemos hacer --amend</p>
<a href="https://lh3.googleusercontent.com/-vkUTNS19ZRU/WcDbBQnDFuI/AAAAAAAAFHw/llSZqtqdMAcnwmoTtjWTDN472tPUseVxACHMYCw/s1600-h/clip_image022%255B5%255D"><img width="180" height="124" title="clip_image022" style="margin: 0px; display: inline; background-image: none;" alt="clip_image022" src="https://lh3.googleusercontent.com/--fpjXQI5xuk/WcDbB6QVx5I/AAAAAAAAFH0/zkaT9BSX5V0tADP322Kp9D64WDKsutq7QCHMYCw/clip_image022_thumb?imgmax=800" border="0"></a>
<p>Como decía, el tema está en que hemos reescrito la historia, antes el commit era a59987c y ahora es 3d41599 (de nuevo, será
otro en tu equipo), es decir, son 2 commits completamente diferentes a ojos del repositorio, luego si ya eran públicos
(estaban subidos al remoto), cualquier otro commit que los estuviera referenciado ya no los va a encontrar y habrá lío
garantizado…</p>
<p>Vamos a reproducir un lío gordo y así lo vemos.</p>
<pre class="brush: bash;">cd C:\Temp
rm -rf *
mkdir central
cd central
git init --bare
cd ..
mkdir example1
cd example1
echo F1 > F1.txt
git init
git add .
git commit -m "C1"
echo F2 > F2.txt
git add .
git commit -m "C2"
git remote add origin C:\Temp\central
git push -u origin master
cd ..
git clone C:\Temp\central example2
cd example2
echo F3 > F3.txt
git add .
git commit -m "C3"
</pre>
<p>Aquí estamos a nivel, todo bien.</p>
<p>En example1</p>
<a href="https://lh3.googleusercontent.com/-PxkpYg2yRX8/WcDbCNSRXcI/AAAAAAAAFH4/-_f6eozokv0XKLOn2-8vDIJDq-TtkW4fwCHMYCw/s1600-h/clip_image023%255B5%255D"><img width="244" height="35" title="clip_image023" style="margin: 0px; display: inline; background-image: none;" alt="clip_image023" src="https://lh3.googleusercontent.com/-aLAmayaPYig/WcDbCuGufOI/AAAAAAAAFH8/FKjvRSoPjUIRuIiu6ksWWeNzMyRCpmwywCHMYCw/clip_image023_thumb?imgmax=800" border="0"></a>
<p>En example2, C3 tiene como padre el commit C2 </p>
<a href="https://lh3.googleusercontent.com/-p1_d1ct8t9s/WcDbC5cPfCI/AAAAAAAAFIA/AHr5cPfw2Egvl6F_7rptzHBLSnAxmwcoACHMYCw/s1600-h/clip_image024%255B5%255D"><img width="244" height="48" title="clip_image024" style="margin: 0px; display: inline; background-image: none;" alt="clip_image024" src="https://lh3.googleusercontent.com/-AAeTIhr6wg4/WcDbEhUMEgI/AAAAAAAAFIE/fgYjYVc_fLsj46HaaS9ZYYldexvY_5uSACHMYCw/clip_image024_thumb?imgmax=800" border="0"></a>
<p>Pero ahora el señor de example1 va a reescribir la historia haciendo un commit --amend y entonces C2 ya no será el mismo
commit sino algún otro con un identificador distinto </p>
<pre class="brush: bash;">cd ..
cd example1
echo F2_1 > F2_1.txt
git add .
git commit --amend -m "C2_updated”
</pre>
<p>Y en example1 tenemos lo siguiente:</p>
<a href="https://lh3.googleusercontent.com/-nBI9x4ps0aU/WcDbE2n-OCI/AAAAAAAAFII/5dwJkV7VZioU-x7vcku5FyVpwkwln51JwCHMYCw/s1600-h/clip_image025%255B5%255D"><img width="244" height="54" title="clip_image025" style="margin: 0px; display: inline; background-image: none;" alt="clip_image025" src="https://lh3.googleusercontent.com/-Yit-9BIlYhw/WcDbFWB3m-I/AAAAAAAAFIM/EFAJQBNaOSYFyP0q0YZhnU1_0Wnmgh-lwCHMYCw/clip_image025_thumb?imgmax=800" border="0"></a>
<p>¡Uy, que feo!, aparece C2_updated (a quien apunta master) pero sigue estando C2 (a quien apunta origin/master). De hecho,
C2 tiene el mismo identificador, sigue estando ahí porque está en el remoto (es público).</p>
<p>Lógicamente tampoco el señor de example1 puede hacer un git push, no está al día, origin/master ya no está apuntado a master.</p>
<p>¿Qué opciones tiene example1? Pues hacer merge de origin/master en master </p>
<pre class="brush: bash;">git merge origin/master master
</pre>
<p>Que podría o no darle un conflicto en función de que como haya sido su --amend, en nuestro ejemplo no dará conflicto, hemos
añadido un fichero.</p>
<a href="https://lh3.googleusercontent.com/-U9IsDDV-BuQ/WcDbF8eHcuI/AAAAAAAAFIQ/keKEqokkMLkuJHleD32hnNfQjEe2S4S9gCHMYCw/s1600-h/clip_image026%255B5%255D"><img width="244" height="46" title="clip_image026" style="margin: 0px; display: inline; background-image: none;" alt="clip_image026" src="https://lh3.googleusercontent.com/-BjZsa5HA3-k/WcDbGFjC0LI/AAAAAAAAFIU/TSDC6VBNde4tgsZDRSJBQRt_p4yA7vsfQCHMYCw/clip_image026_thumb?imgmax=800" border="0"></a>
<p>Estoy seguro de que el señor de example1 no quería este commit de merge… pero bueno, igualmente hace un push</p>
<pre class="brush: bash;"> git push
</pre>
<p>¿Y cómo queda el señor de example2?</p>
<pre class="brush: bash;">cd ..
cd example2
git fetch
</pre>
<p>Pues directamente le han hecho el lío, lo que él pensaba era un valor seguro, un commit público que estaba en el remoto grabado
a piedra, pues ya no lo es tanto, sigue estando ahí pero ya no es lo mismo… se ha roto la regla de oro, no reescribir
la historia en commits públicos</p>
<a href="https://lh3.googleusercontent.com/-ze85Qk1eT2c/WcDbGtdVkII/AAAAAAAAFIY/kQN8wiV72vUdnmUCGFyLH1tqvyUT0-YtwCHMYCw/s1600-h/clip_image027%255B5%255D"><img width="244" height="47" title="clip_image027" style="margin: 0px; display: inline; background-image: none;" alt="clip_image027" src="https://lh3.googleusercontent.com/-OcnWyXJsaVQ/WcDbHAsPqrI/AAAAAAAAFIc/H8pEdVNS8mMpDGyGKhB9ftcIbYEPjXbjgCHMYCw/clip_image027_thumb?imgmax=800" border="0"></a>
<pre class="brush: bash;">git pull
</pre>
<a href="https://lh3.googleusercontent.com/-Zb5Oi1PyUlE/WcDbHeKHDiI/AAAAAAAAFIg/mPxkAiF_5v04qEHo2k3pnggCeVuelrR6wCHMYCw/s1600-h/clip_image028%255B5%255D"><img width="244" height="55" title="clip_image028" style="margin: 0px; display: inline; background-image: none;" alt="clip_image028" src="https://lh3.googleusercontent.com/-0fLgPcsSfbg/WcDbINcxnAI/AAAAAAAAFIk/oMnnB_bMn3YiLu04VNh-jtpMJj9gvfHXwCHMYCw/clip_image028_thumb?imgmax=800" border="0"></a>
<p>Y ahora ya puede hacer</p>
<pre class="brush: bash;">git push
</pre>
<p>Gracias, señor de example1, acabas de afear la historia del repositorio y además has agregado una complejidad que me acordaré
de ti por siempre… por lo menos no has hecho un git push –force, en fin…
<p>
<p>La idea es que reescribir commits que son públicos es una pésima idea y fuente de problemas, no se hace y punto,
es otro mantra.</p>
<p>Y por fin llegamos al rebase, que mola mucho… pero reescribe la historia, así que cuidado.</p>
<p>Rebasar es “mover” commits a un nuevo commit base.</p>
<p>Para integrar cambios, se puede hacer con merge o con rebase + merge, que siempre será fast-forward… y es aquí donde
está el debate ¿merge o rebase? Según a quién preguntes te dirá una cosa u otra</p>
<p>Se hace rebase por alguno de estos motivos:</p>
<ul>
<li>Mantener una historia “lineal” del proyecto para mejorar la legibilidad
<ul>
<li>Aunque se pierda trazabilidad en la integración de ramas porque no hay commits de merge</li>
</ul>
</li>
<li>Hacer un clean-up de mi historia local con un rebase interactivo</li>
</ul>
<p>Hacer un clean-up de mi historia local con un rebase interactivo.
<p><b></b>
<p>Hacer un clean-up no parece que suscite debate (siempre y cuando se haga en local, en remoto no se hace,
recuerda que reescribe la historia, el mantra…). Lo hacemos porque en local podríamos haber hecho n commits
(porque nos apeteció, es tu repo, ahí no manda nadie) pero cuando queremos compartir los cambios con
el resto (subir al remoto) queremos pasarle un poco la mopa para que quede todo bonito y apañado.</p>
<p>Por ejemplo, estoy trabajando en mi local con aparente desgana:</p>
<pre class="brush: bash;">cd C:\Temp
rm -rf *
mkdir example
cd example
git init
echo example > example.txt
git add .
git commit -m "Initial commit"
echo F2 > F2.txt
git add .
git commit -m "C2"
echo F3 > F3.txt
git add .
git commit -m "C3"
echo F4 > F4.txt
git add .
git commit -m "C4"
echo F_fake > F_fake.txt
git add .
git commit -m "Fake"
echo F5 > F5.txt
git add .
git commit -m "C5"
echo F1 > F1.txt
git add .
git commit -m "C1"
</pre>
<a href="https://lh3.googleusercontent.com/-d9SA79JOS-A/WcDbIUCQr8I/AAAAAAAAFIo/njkcHGPjQ2s8NwexzaJ49EBJg7SxGL4YQCHMYCw/s1600-h/clip_image029%255B5%255D"><img width="197" height="135" title="clip_image029" style="margin: 0px; display: inline; background-image: none;" alt="clip_image029" src="https://lh3.googleusercontent.com/-0vPVufipqkQ/WcDbI-mEqpI/AAAAAAAAFIs/Clo4z7ZYbSkLpV9-vpq-07D2NF33NriEACHMYCw/clip_image029_thumb?imgmax=800" border="0"></a>
<p>Sinceramente, no voy a subir eso al remoto para que lo vean mis compañeros, los commits están desordenados,
tengo un commit fake y además no me he preocupado por los comentarios, ¡ni yo sé que he incluido en cada
commit1</p>
<p>Se podría hacer el rebase interactivo por comandos, pero con franqueza, una herramienta como SourceTree nos
facilitará la vida.</p>
<a href="https://lh3.googleusercontent.com/-9JO7zB8ll0U/WcDbJI6V2gI/AAAAAAAAFIw/tgaj_xFQ3owTRkpFn4oIImRHOCcExknVgCHMYCw/s1600-h/clip_image030%255B5%255D"><img width="244" height="209" title="clip_image030" style="margin: 0px; display: inline; background-image: none;" alt="clip_image030" src="https://lh3.googleusercontent.com/-6oRBz3MFua4/WcDbJpB_92I/AAAAAAAAFI0/rO3n0gNX0vEPj8Trr6pGICtK11n5pRVQACHMYCw/clip_image030_thumb?imgmax=800" border="0"></a>
<p>Y pasamos de</p>
<a href="https://lh3.googleusercontent.com/-1PCHouX8It0/WcDbKed3a_I/AAAAAAAAFI4/VmvCiEvbvAQdPapTBFdv29GAopvEyUXLACHMYCw/s1600-h/clip_image031%255B5%255D"><img width="242" height="125" title="clip_image031" style="margin: 0px; display: inline; background-image: none;" alt="clip_image031" src="https://lh3.googleusercontent.com/-3tDqabWCXWA/WcDbKwjQ7rI/AAAAAAAAFI8/In2xabOpMnE3HVR6_NS9PBf5TKUyg1QagCHMYCw/clip_image031_thumb?imgmax=800" border="0"></a>
<p>A esto otro (haciendo squash, ordenando, eliminado y cambiando mensajes de commits)</p><p>
<a href="https://lh3.googleusercontent.com/-rKE_xnNYjU4/WcDbLI_HwXI/AAAAAAAAFJA/zFYrRI1m1FMJJ7t476ClGufJpab0b8l_gCHMYCw/s1600-h/clip_image032%255B5%255D"><img width="244" height="76" title="clip_image032" style="margin: 0px; display: inline; background-image: none;" alt="clip_image032" src="https://lh3.googleusercontent.com/-TBEWWmEsUr4/WcDbLrARvwI/AAAAAAAAFJE/S26_vLtA85cQiZA2bi0Qcw8t5e9f849ewCHMYCw/clip_image032_thumb?imgmax=800" border="0"></a></p><p>
<a href="https://lh3.googleusercontent.com/-guRTrc_5tSY/WcDbMOoHM9I/AAAAAAAAFJI/eRpQhLHPklAXWmKvVqcS6BOHE2f5PYiNACHMYCw/s1600-h/clip_image033%255B5%255D"><img width="244" height="78" title="clip_image033" style="margin: 0px; display: inline; background-image: none;" alt="clip_image033" src="https://lh3.googleusercontent.com/-GpsSVxXkxbQ/WcDbMue1CZI/AAAAAAAAFJM/uZFm9uEFNPYW9cTaDlTyvycMDPCwtV8OQCHMYCw/clip_image033_thumb?imgmax=800" border="0"></a>
</p><p>Esto es otra cosa, ¡ya puedo subir mis cambios!</p>
<p>Si quieres hacerlo interactivo por consola, puedes hacerlo con git rebase [-i] <commit_base>, pero
se va a abrir casi seguro VIM y ahí te apañes tú… Si por ejemplo “Initial commit” tuviera el identificador
c7f0016</p>
<pre class="brush: bash;">git rebase -i c7f0016
</pre>
<a href="https://lh3.googleusercontent.com/-KM_vAPT2gQo/WcDbM0TkCrI/AAAAAAAAFJQ/zuGYBVStHMsbitSbhhegU9tAvkksnFL6wCHMYCw/s1600-h/clip_image035%255B5%255D"><img width="244" height="129" title="clip_image035" style="margin: 0px; display: inline; background-image: none;" alt="clip_image035" src="https://lh3.googleusercontent.com/-LMr2DSAOz2o/WcDbNfhTgdI/AAAAAAAAFJU/3b9mK0ACuM8Ien6q5N_ayeVA-dtaLXB-wCHMYCw/clip_image035_thumb?imgmax=800" border="0"></a>
<p>El otro sabor de rebase (el que entra en debate con merge) es el “no interactivo”, el que se rige por el
mantra “estoy en y rebaso a”. Lo veremos con un ejemplo típico de feature branch.</p>
<pre class="brush: bash;">cd C:\Temp
rm -rf *
mkdir example
cd example
git init
echo example > example.txt
git add .
git commit -m "Initial commit"
echo A1 > A1.txt
git add .
git commit -m "A1"
echo A2 > A2.txt
git add .
git commit -m "A2"
git checkout -b features/B
echo B1 > B1.txt
git add .
git commit -m "B1"
echo B2 > B2.txt
git add .
git commit -m "B2"
echo B3 > B3.txt
git add .
git commit -m "B3"
git checkout master
echo A3 > A3.txt
git add .
git commit -m "A3"
echo A4 > A4.txt
git add .
git commit -m "A4"
git checkout features/B
</pre>
<a href="https://lh3.googleusercontent.com/-GFvFIIgzRpg/WcDbNq124KI/AAAAAAAAFJY/elLajp9Zq-4UYAD1dDHjVi06AwE0aQACgCHMYCw/s1600-h/clip_image036%255B5%255D"><img width="212" height="156" title="clip_image036" style="margin: 0px; display: inline; background-image: none;" alt="clip_image036" src="https://lh3.googleusercontent.com/-UqukA23uit0/WcDbOEhO6sI/AAAAAAAAFJc/AhBNSq3TOLEQ5llBFlHao1aBJWhAz8MrgCHMYCw/clip_image036_thumb?imgmax=800" border="0"></a>
<pre class="brush: bash;">git rebase master
</pre><p>
<a href="https://lh3.googleusercontent.com/-tWrowfTPuM8/WcDbOXOl3SI/AAAAAAAAFJg/D6rbsix3S1AmWITcDoYrK71Amzz_3BWhgCHMYCw/s1600-h/clip_image037%255B5%255D"><img width="244" height="51" title="clip_image037" style="margin: 0px; display: inline; background-image: none;" alt="clip_image037" src="https://lh3.googleusercontent.com/-VkUh5vzW4XQ/WcDbOwRZl9I/AAAAAAAAFJk/2WKrY99V9HIDcjmasq4__mPXwEFaVwIswCHMYCw/clip_image037_thumb?imgmax=800" border="0"></a></p><p>
<a href="https://lh3.googleusercontent.com/-PWpLLtsWTcU/WcDbPVhnyuI/AAAAAAAAFJo/hDicr3_9ilknVvLDZY7q2qa1mOJSWKJcgCHMYCw/s1600-h/clip_image038%255B5%255D"><img width="199" height="159" title="clip_image038" style="margin: 0px; display: inline; background-image: none;" alt="clip_image038" src="https://lh3.googleusercontent.com/-2Bqr6KBhHIs/WcDbP36r1kI/AAAAAAAAFJs/69yu26myx9IWquiAfb6w0p3_4PbuSKvIgCHMYCw/clip_image038_thumb?imgmax=800" border="0"></a>
</p><p>El rebase ha cogido todos los commits de features/B y los ha puesto a continuación del último commit de master,
todo lineal, muy bonito… pero ha reescrito la historia de los commits de features/B</p>
<p>Si estás trabajando sólo tú en esa rama en este momento (y no hay ningún compañero pendiente de subir cambios
al repositorio en esa rama, y tampoco nadie creó una rama a partir de un commit de esa rama), no hay
problema, si no lío…</p>
<p>Desde Source Tree, hubieramos hecho</p><p>
<a href="https://lh3.googleusercontent.com/-OjHO2G8SYsc/WcDbQB3CO1I/AAAAAAAAFJw/vPSVkpLg4EoYQs2qZQdVwQcqLignFR19wCHMYCw/s1600-h/clip_image039%255B5%255D"><img width="244" height="190" title="clip_image039" style="margin: 0px; display: inline; background-image: none;" alt="clip_image039" src="https://lh3.googleusercontent.com/-s92D1Mo5MSE/WcDbQ9ZgQwI/AAAAAAAAFJ0/fycl2iyTXh0m1kqivSdXq61XEh8S5hntQCHMYCw/clip_image039_thumb?imgmax=800" border="0"></a></p><p>
<a href="https://lh3.googleusercontent.com/-_i6mFcVKJH0/WcDbRe0o9WI/AAAAAAAAFJ4/6WIRgXvfPlcru2BvJsdP7OyfezBriWnewCHMYCw/s1600-h/clip_image040%255B5%255D"><img width="244" height="85" title="clip_image040" style="margin: 0px; display: inline; background-image: none;" alt="clip_image040" src="https://lh3.googleusercontent.com/-AXJyqpECo_w/WcDbRtOT4II/AAAAAAAAFJ8/N7_iojxECGMUrFswpUktfXf2QHE5WBHaACHMYCw/clip_image040_thumb?imgmax=800" border="0"></a>
</p><p>Por cierto, si el rebase da conflictos, los resolvemos y después git rebase --continue o git rebase --abort</p>
<p>Y si queremos deshacer el rebase podemos usar git reset --hard ORIG_HEAD <a href="https://stackoverflow.com/a/692763">https://stackoverflow.com/a/692763</a>
<p>
<p>Y después de este tipo de rebase tenemos que hacer un merge, que será siempre fast-forward</p>
<pre class="brush: bash;">git checkout master
git merge features/B
</pre>
<a href="https://lh3.googleusercontent.com/-61xj62sn9gY/WcDbSGTjkqI/AAAAAAAAFKA/I_1dczQsUH0e3hg-FZoyzehH6Mtrq5nNACHMYCw/s1600-h/clip_image041%255B5%255D"><img width="244" height="133" title="clip_image041" style="margin: 0px; display: inline; background-image: none;" alt="clip_image041" src="https://lh3.googleusercontent.com/-FaJI2OoBPBY/WcDbSuWBMEI/AAAAAAAAFKE/vrbOpOiJN042D31tWy3zYJ-RulzD_7v2wCHMYCw/clip_image041_thumb?imgmax=800" border="0"></a>
<p>Y nada más, espero que te sirva este post porque, lo que es seguro, es que yo sí volveré a él cada
vez que tenga que hablar sobre este tema, seguramente para refrescar el porqué de las cosas o
bien para actualizarlo porque he descubierto que algo no funcionaba como pensaba.</p><p>Un saludo!</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com0tag:blogger.com,1999:blog-4364815135056214516.post-7384469346013961182017-05-30T11:18:00.001-07:002017-05-30T11:21:58.382-07:00JSON en SQL Server 2016<p>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.</p>
<p>Aunque hay una excelente documentación al respecto en <a href="https://docs.microsoft.com/en-us/sql/relational-databases/json/json-data-sql-server">JSON Data (SQL Server)</a>,
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.</p>
<p>Lo primero es que no hay un tipo <em>json</em>, en realidad trabajaremos con <em>nvarchar</em> y todo la magia ocurrirá a
través de nuevas clausulas y funciones.</p>
<p>Para organizar el post, voy a plantear un escenario donde, primero importaremos datos desde un fichero <em>.json</em>, 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.</p>
<p>El script SQL necesario para todas las pruebas es el siguiente:</p>
<pre class="brush: sql;">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]
</pre>
<p>Para importar datos desde un fichero <em>.json</em> podemos usar la función <a href="https://msdn.microsoft.com/es-es/library/ms190312.aspx">OPENROWSET</a>
con el parámetro SINGLE_CLOB, que lee y devuelve su contenido como una única fila y columna <em>BulkColumn</em> de tipo nvarchar(max). Además, tendremos
que usar la función <a href="https://docs.microsoft.com/en-us/sql/relational-databases/json/convert-json-data-to-rows-and-columns-with-openjson-sql-server">OPENJSON</a>
que convierte JSON en filas y columnas.</p>
<p>Nuestro fichero <em>.json</em> será como sigue</p>
<pre class="brush: js;">[
{
"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"
}
}
]
}
]
</pre>
<p>Ahora podemos usar OPENROWSET y OPENJSON</p>
<pre class="brush: sql;">SELECT BulkColumn, [key], [value], [type]
FROM OPENROWSET (BULK 'C:\panicoenlaxbox\data.json', SINGLE_CLOB) AS T
CROSS APPLY OPENJSON(BulkColumn)
</pre>
<p>OPENROWSET devuelve la columna <em>BulkColumn</em>, OPENJSON devuelve las columnas <em>key</em>, <em>value</em> y <em>type</em>.</p>
<p><a href="https://lh3.googleusercontent.com/-6q7X4gtSc_U/WS23XfFEJGI/AAAAAAAAEvk/mrA4XOZSQvcXts72sapY-yL18iuLOEdegCHM/s1600-h/image%255B11%255D"><img width="546" height="56" title="image" style="display: inline; background-image: none;" alt="image" src="https://lh3.googleusercontent.com/-CigjonkMdBc/WS23XuuzpOI/AAAAAAAAEvo/mxkJjlDsNwMLgwHOjhKfLo7vU7j0yzLcACHM/image_thumb%255B4%255D?imgmax=800" border="0"></a></p>
<p>Si en fichero<em> .json</em> tuviera un sólo objeto en vez de un array, la salida nos ayudaría a entender mejor como funciona OPENJSON</p>
<pre class="brush: js;">{
"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"
}
}
]
}
</pre>
<p><a href="https://lh3.googleusercontent.com/-lSDzL1GV1i0/WS23YBvPyLI/AAAAAAAAEvs/je75GLt4VEcxbsgkVluZOz15WO_PJeL8wCHM/s1600-h/image%255B7%255D"><img width="545" height="74" title="image" style="margin: 0px; display: inline; background-image: none;" alt="image" src="https://lh3.googleusercontent.com/-Sf8I8kIg2N4/WS23Yk_ot1I/AAAAAAAAEvw/AlsLWNX3OGwSutp9Dd56bD3aEwvrxhdVwCHM/image_thumb%255B2%255D?imgmax=800" border="0"></a></p>
<p>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</p>
<pre class="brush: sql;">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
</pre>
<p><a href="https://lh3.googleusercontent.com/-D0NXyBM9rv0/WS23Y_tT_aI/AAAAAAAAEv0/hGHVYUnK_hkkbL3QTUVSTDMG6kRYmispQCHM/s1600-h/image%255B15%255D"><img width="548" height="67" title="image" style="display: inline; background-image: none;" alt="image" src="https://lh3.googleusercontent.com/-FdljwrxPTfU/WS23ZZekSuI/AAAAAAAAEv4/htYaLIw8srsLFT25f-4KA2tOZmCcwrCGQCHM/image_thumb%255B6%255D?imgmax=800" border="0"></a></p>
<p>Ahora ya sí podemos insertar estos datos leídos del fichero <em>.json</em> en nuestras tablas</p>
<pre class="brush: sql;">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;
</pre>
<p>Si hablamos ahora de formatear consultas, tendremos que usar la clausula FOR JSON</p>
<pre class="brush: sql;">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
</pre>
<p>Que devuelve</p>
<pre class="brush: js;"> [{
"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"
}]
}]
}]
</pre>
<p>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 <em>.json</em> que usamos al comienzo para importar los datos</p>
<pre class="brush: sql">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
</pre>
<p>En cuanto a que podemos hacer para consultar datos JSON almacenados en una columna, encontramos varias funciones:
<ul>
<li>ISJSON</li>
<li>JSON_VALUE</li>
<li>JSON_QUERY</li>
<li>JSON_MODIFY</li>
</ul>
<p>ISJSON valida que el texto es JSON válido</p>
<p>JSON_VALUE extrae un valor desde JSON</p>
<p>JSON_QUERY extrae como texto, un objeto u array desde JSON</p>
<p>JSON_MODIFY permite modificar JSON y devuelve el resultado</p>
<p>Lo más sencillo será agregar una nueva columna para poder jugar con ella</p>
<pre class="brush: sql;">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;
</pre>
<p>Ahora podemos ejecutar las siguientes consultas</p>
<pre class="brush: sql;">--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"]
</pre>
<p>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</p>
<pre class="brush: sql;">SELECT JSON_VALUE(SecurityContext, '$.Enabled') FROM Orders
WHERE JSON_VALUE(SecurityContext, '$.Enabled') = 'true'</pre><p><a href="https://lh3.googleusercontent.com/-vpw1LyaIj9c/WS23ZvkgiwI/AAAAAAAAEv8/dvSlmfTY8Qs00VyOmHJDPe3r1IX2yZwHQCHM/s1600-h/image%255B27%255D"><img width="538" height="64" title="image" style="margin: 0px; display: inline; background-image: none;" alt="image" src="https://lh3.googleusercontent.com/-4voBSFfO99k/WS23aDBsmII/AAAAAAAAEwA/JJ_yliLxKMsJQJjT-wEdG2U0HGYCaOv2QCHM/image_thumb%255B11%255D?imgmax=800" border="0"></a>
</p>
<p>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</p>
<pre class="brush: sql;">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'
</pre>
<p><a href="https://lh3.googleusercontent.com/-LSxMhOITjwA/WS23aailVpI/AAAAAAAAEwE/bQrSKKD1QjkslLEKM1aTv7ft4ebuV76ywCHM/s1600-h/image%255B22%255D"><img width="539" height="66" title="image" style="display: inline; background-image: none;" alt="image" src="https://lh3.googleusercontent.com/-sAmgQV1ugnQ/WS23ahfp17I/AAAAAAAAEwI/KOoW9arUEfEO_UXjBvcQqnSpMUgb6Li6gCHM/image_thumb%255B9%255D?imgmax=800" border="0"></a></p>
<p>Un saludo!</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com0tag:blogger.com,1999:blog-4364815135056214516.post-80580383656869962712017-03-21T05:43:00.001-07:002017-09-19T05:19:34.238-07:00Equipos, áreas e iteraciones en VSO<p>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 <em>disclaimer</em>. <p>La única herramienta que he usado hasta la fecha para llevar a cabo tan magna tarea ha sido Visual Studio Team Services (VSTS) <a title="https://www.visualstudio.com/es/vso/" href="https://www.visualstudio.com/es/vso/">https://www.visualstudio.com/es/vso/</a> (o Visual Studio Online o Team Foundation Service como se le conocía anteriormente… ya me han corregido en Twitter <a title="https://twitter.com/jc_quijano/status/844173841949691905" href="https://twitter.com/jc_quijano/status/844173841949691905">https://twitter.com/jc_quijano/status/844173841949691905</a>). <p>El flujo era sencillo: crear un nuevo proyecto y agregar, mejor o peor redactados, un buen saco de PBIs y a partir de ahí y fingiendo hacer SCRUM, organizar el trabajo en sprints y tirar <em>pa’lante</em>. <p>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 <em>googleado</em> 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. <p>Para hilar la explicación del post, nuestra empresa se llamará <a href="https://es.wikipedia.org/wiki/Corporaci%C3%B3n_Acme">Acme</a> (Team Project Collection) y crearemos un proyecto (Team Project) con el mismo nombre y escogeremos el <a href="https://www.visualstudio.com/en-us/docs/work/guidance/choose-process">proceso</a> SCRUM. <p>Recién creado, el nuevo proyecto contiene lo siguiente: <ul> <li>7 Iterations en una estructura jerárquica. </li><ul><li>Su nodo principal se llama Acme y le cuelgan 6 nodos llamadas Sprint 1-6.</li></ul> <li>1 Area llamada Acme.</li> <li>Un Team con el nombre del proyecto + Team. Es decir, Acme Team.</li> <li>El equipo Acme Team tiene como área por defecto el área Acme y tiene asignadas las 6 iteraciones Sprint 1-6.</li></ul> <p>Como verás, es todo muy “Acme”, sobredosis de nombre. <p>En la pestaña Overview del proyecto <a href="https://acmecorporation.visualstudio.com/Acme/_admin">https://acmecorporation.visualstudio.com/Acme/_admin</a> tenemos los equipos (el segmento <em>Acme/_admin</em> es importante, nos dice que estamos a nivel de proyecto): <p><a href="https://lh3.googleusercontent.com/-021oSxIwM4A/WNEggv2iwYI/AAAAAAAAEm0/D-ZQ8ECOle4/s1600-h/clip_image001%25255B4%25255D.png"><img width="348" height="136" title="clip_image001" style="border: 0px currentcolor; border-image: none; padding-top: 0px; padding-right: 0px; padding-left: 0px; display: inline; background-image: none;" alt="clip_image001" src="https://lh3.googleusercontent.com/-l-Bl07hOoFc/WNEgg6g6tEI/AAAAAAAAEm4/bCPO0_rLx4E/clip_image001_thumb%25255B1%25255D.png?imgmax=800" border="0"></a> <p>En la pestaña Work encontramos Iterations y Areas: <p><a href="https://lh3.googleusercontent.com/-7ZSnsy6bIg4/WNEghZSig4I/AAAAAAAAEm8/Kvg1GeAByCM/s1600-h/clip_image002%25255B4%25255D.png"><img width="351" height="279" title="clip_image002" style="border: 0px currentcolor; border-image: none; padding-top: 0px; padding-right: 0px; padding-left: 0px; display: inline; background-image: none;" alt="clip_image002" src="https://lh3.googleusercontent.com/-sxzBByj0jx8/WNEghxcuz9I/AAAAAAAAEnA/3NeAzj4rTkQ/clip_image002_thumb%25255B1%25255D.png?imgmax=800" border="0"></a> <p><a href="https://lh3.googleusercontent.com/-Ons6gdE32fo/WNEgiGRTDsI/AAAAAAAAEnE/ufby1p_QppA/s1600-h/clip_image003%25255B4%25255D.png"><img width="355" height="150" title="clip_image003" style="border: 0px currentcolor; border-image: none; padding-top: 0px; padding-right: 0px; padding-left: 0px; display: inline; background-image: none;" alt="clip_image003" src="https://lh3.googleusercontent.com/-OMafs-7Qr70/WNEgikOq4rI/AAAAAAAAEnI/71yF991ogq4/clip_image003_thumb%25255B1%25255D.png?imgmax=800" border="0"></a> <p>Si navegamos al equipo, podemos ver como el segmento de la url cambia desde <em>Acme/_admin</em> a <em>Acme/Acme%20Team/_admin</em>. Aunque la interfaz nos ayuda a saber dónde estamos, viendo la url salimos de dudas, ahora estamos en el equipo, no en el proyecto. <p>En la pestaña Work del equipo encontramos de nuevo Iterations y Areas: <p><a href="https://lh3.googleusercontent.com/-ZgJoneGvz78/WNEgi4OZ7qI/AAAAAAAAEnM/9Qnz0YFZJIs/s1600-h/clip_image005%25255B4%25255D.jpg"><img width="363" height="149" title="clip_image005" style="border: 0px currentcolor; border-image: none; padding-top: 0px; padding-right: 0px; padding-left: 0px; display: inline; background-image: none;" alt="clip_image005" src="https://lh3.googleusercontent.com/-136Ay2IrIc8/WNEgjs3XnyI/AAAAAAAAEnQ/aCjAanZHU9Y/clip_image005_thumb%25255B1%25255D.jpg?imgmax=800" border="0"></a> <p><a href="https://lh3.googleusercontent.com/-Q4sJzR33_hA/WNEgj2HBwxI/AAAAAAAAEnU/J6FxjC9alJo/s1600-h/clip_image006%25255B4%25255D.png"><img width="367" height="96" title="clip_image006" style="border: 0px currentcolor; border-image: none; padding-top: 0px; padding-right: 0px; padding-left: 0px; display: inline; background-image: none;" alt="clip_image006" src="https://lh3.googleusercontent.com/-Zi96lilj8F8/WNEgkVQ99lI/AAAAAAAAEnY/NbJLqq7qO1E/clip_image006_thumb%25255B1%25255D.png?imgmax=800" border="0"></a> <p>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 <em>Acme/_workitems</em> o a <em>Acme/Acme%20Team/_workitems</em>, la query que hay por debajo es la misma. <p>En el caso de “Backlog items”: <p><a href="https://lh3.googleusercontent.com/-1qoxlaww8-Q/WNEiSTAFh-I/AAAAAAAAEn0/N_QP_SzJOgk/s1600-h/image%25255B9%25255D.png"><img width="525" height="402" title="image" style="border: 0px currentcolor; border-image: none; padding-top: 0px; padding-right: 0px; padding-left: 0px; display: inline; background-image: none;" alt="image" src="https://lh3.googleusercontent.com/-zJnnofnhzf8/WNEiS0pYphI/AAAAAAAAEn4/dzzLJoXsbgk/image_thumb%25255B5%25255D.png?imgmax=800" border="0"></a> <p>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 <p><a href="https://lh3.googleusercontent.com/-Qni9vWbxFHQ/WNEiTL74JhI/AAAAAAAAEn8/CYxSvcMYMRM/s1600-h/image%25255B14%25255D.png"><img width="526" height="385" title="image" style="border: 0px currentcolor; border-image: none; padding-top: 0px; padding-right: 0px; padding-left: 0px; display: inline; background-image: none;" alt="image" src="https://lh3.googleusercontent.com/-GwrZ5yl-wnM/WNEiTf0sorI/AAAAAAAAEoA/PazyDaawRkk/image_thumb%25255B8%25255D.png?imgmax=800" border="0"></a> <p>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. <p>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 en el proyecto 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. <p>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 no excluyentes.<p>A modo de resumen: <ul> <li>Un Work Item es un elemento de trabajo cuya propiedad “Work Item Type” determina de qué tipo es. </li><ul><li>Un Product Backlog Item es un Work Item… una Task o un Bug también lo son.</li></ul> <li>Un Work Item puede relacionarse con otro Work Item. </li><ul><li>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.</li></ul> <li>Un Work Item siempre tiene asignado un “Iteration Path” y un “Area Path”.</li> <li>Una iteración es un evento relacionado con el tiempo, normalmente será un sprint, pero podría ser cualquier otra cosa.</li> <li>Un área es una división lógica del Backlog.</li> <li>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.</li> <li>Un equipo tiene acceso a n iteraciones.</li> <li>Un equipo tiene acceso a n áreas.</li> <li>Por ende, un equipo verá los Work Items que tengan asignados esas iteraciones o áreas.</li></ul> <p>Si, por ejemplo, Acme Corporation tuviera dos productos, Cohetes e Imanes ¿Cómo organizar esto en un solo proyecto? <p>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. <p>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: <p><a href="https://lh3.googleusercontent.com/-6HK7qeLYpZg/WNEiT0DO0pI/AAAAAAAAEoE/3BXeiWdPgjA/s1600-h/image%25255B19%25255D.png"><img width="475" height="60" title="image" style="border: 0px currentcolor; border-image: none; padding-top: 0px; padding-right: 0px; padding-left: 0px; display: inline; background-image: none;" alt="image" src="https://lh3.googleusercontent.com/-1X7RUMwDbYU/WNEiUBbHNGI/AAAAAAAAEoI/bpfdfgATavo/image_thumb%25255B11%25255D.png?imgmax=800" border="0"></a> <p>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. <p>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. <p>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. <p>¿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. <p>La distribución quedaría así: <ul> <li>Acme Team – Acme (include sub-areas)</li> <li>Cohetes Team – Acme\Cohetes (include sub-areas)</li> <li>Imanes Team – Acme\Imanes (include sub-areas)</li></ul> <p>Ni que decir tiene, que un usuario puede pertenecer a cualquier número de equipos, podría construir tanto cohetes como imanes. <p>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. <p>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: <ul> <li>Acme</li> <ul> <li>Release 1</li> <ul> <li>Sprint 1</li> <li>Sprint 2</li></ul></ul></ul> <p>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”. <p>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…). <p>Finalmente, en la pestaña Work y en el menú izquierdo aparecerán las iteraciones asignadas (una Current y el resto Futures). <p>¿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. <p>Respecto a iteraciones y en el contexto de un equipo, es importante entender las opciones “Default Iteration” y “Backlog iteration”. <p>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”. <p>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). <p>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. <p>Llegados a este punto, ya hemos resuelto como se relacionan Team Projects, Work Items, Teams, Iteration Paths y Areas. <p>Por sacarle punta al asunto, los inconvenientes que por ahora veo sobre tener equipos por áreas proyectos serían: <ul> <li>La velocidad es por área.</li> <li>Si alguien trabaja al mismo tiempo en esprines en distintos equipos (cosa que no debería pasar idealmente), tendrá que dividir su capacidad y estar al tanto de dos esprines, no sólo uno.</li> <li>Con el tiempo y si el número de áreas/proyectos crece, podría haber más equipos que gente trabajando. ¡Awesome!</li></ul> <p>Otro inconveniente que merece su propio punto y aparte es cómo ordenar la prioridad del backlog si un equipo tiene varias áreas asignadas. Es decir, si vamos al backlog del equipo veremos todos los PBIs de las áreas a las que tiene acceso el equipo, y priorizar allí una sola área de muchas existentes no es tarea sencilla, mucho ruido. La primera solución que se le ocurriría a cualquiera (yo incluido) sería hacer una query para filtrar por área, pero el problema es que las queries son de sólo-lectura en cuanto a la ordenación. La única solución que hemos visto viable hasta el momento ha sido crear una query que incluya el campo “Backlog priority” (campo orden, por el que se ordena el backlog) y después editar los resultados de la query vía <a href="https://docs.microsoft.com/es-es/vsts/work/office/bulk-add-modify-work-items-excel">Excel</a>. Digo vía excel porque desde allí podemos operar sobre el backlog de forma masiva, porque aunque personalizáramos el proceso de VSTS para incluir el campo “Backlog priority” en la edición de un PBI, no permite editar este campo de forma masiva, con selección múltiple. Sin embargo, a través de excel sí que podemos hacerlo e incluso gente de negocio que no tenga instalado Visual Studio puede hacerlo igualmente descargando el <a href="https://www.visualstudio.com/es/downloads/?rr=https%3A%2F%2Fdocs.microsoft.com%2Fes-es%2Fvsts%2Fwork%2Foffice%2Fbulk-add-modify-work-items-excel">plugin oportuno</a> (<em>Integración de Office® para Team Foundation Server 2017</em>) para agregar la pestaña “Team” a excel y funcionar desde allí.<p>Bueno, espero que le sea de utilidad a alguien este mamotreto, a mí seguro que sí. <p>¡Un saludo! Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com0tag:blogger.com,1999:blog-4364815135056214516.post-68713220849175736212017-01-08T12:58:00.000-08:002017-01-08T13:01:02.765-08:00Personalizar prompt en Bash/Visual Studio Code<p>Ahora que <a href="http://blog.koalite.com/2016/10/la-consola-mola/">la consola mola</a> y he adoptado como animal de compañía a <a href="http://blog.koalite.com/2016/10/la-consola-mola/">Visual Studio Code</a>, me pasa que estoy más tiempo del previsto en el panel de la terminal integrada.</p> <p>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 <em>bash</em> y como personalizarlo </p> <p>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.</p> <p>Para personalizar el prompt tenemos que modificar la variable de entorno <em>PS1</em>, en <a href="http://www.thegeekstuff.com/2008/09/bash-shell-ps1-10-examples-to-make-your-linux-prompt-like-angelina-jolie/">Bash Shell PS1: 10 Examples to Make Your Linux Prompt like Angelina Jolie</a> está muy bien explicado, pero después de hacer algunas pruebas y desesperado por la sintaxis del color, encontré <a href="http://ezprompt.net/">EzPromptEasy Bash PS1 Generator</a> que permite gráficamente construir un prompt nivel 100.</p> <p><a href="https://lh3.googleusercontent.com/-PqcBrgYhHmA/WHKogptVPEI/AAAAAAAAEZc/hzBKj3Z5YTc/s1600-h/Screen%252520Shot%2525202017-01-08%252520at%25252020.07.04.png"><img title="Screen Shot 2017-01-08 at 20.07.04" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Screen Shot 2017-01-08 at 20.07.04" src="https://lh3.googleusercontent.com/-VYGI9yZx8ng/WHKohaANPyI/AAAAAAAAEZg/vxhegqNZp4w/Screen%252520Shot%2525202017-01-08%252520at%25252020.07.04_thumb.png?imgmax=800" width="392" height="143"></a></p> <p>Solucionado ya el tema del prompt, ahora queda ver como agregar color al comando <em>ls</em></p> <p>Lo primero es establecer la variable <em>CLICOLOR</em> a 1 y después en la variable <em>LSCOLORS</em> 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 <a href="http://geoff.greer.fm/lscolors/">LSCOLORS Generator</a></p> <p>Finalmente quedaría un fichero <em>.bash_profile</em> como el siguiente:. </p><pre class="brush: bash;"># 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'</pre>
<p>La terminal de Mac funciona perfecto con este fichero.</p>
<p><a href="https://lh3.googleusercontent.com/-A3BWopPxEa4/WHKoh6ll-BI/AAAAAAAAEZk/mCyo2E1MR20/s1600-h/Screen%252520Shot%2525202017-01-08%252520at%25252020.19.47.png"><img title="Screen Shot 2017-01-08 at 20.19.47" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Screen Shot 2017-01-08 at 20.19.47" src="https://lh3.googleusercontent.com/-gfeGflHb6CU/WHKoiUvVPfI/AAAAAAAAEZo/WyUYNkXLtAM/Screen%252520Shot%2525202017-01-08%252520at%25252020.19.47_thumb.png?imgmax=800" width="323" height="51"></a></p>
<p>Sin embargo Visual Studio Code no se entera, ¿Por qué? Porque <a href="http://www.joshstaiger.org/archives/2005/07/bash_profile_vs.html">.bash_profile es una cosa y .bashrc es otra completamente distinta</a></p>
<p><em>.bash_profile </em>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 <em>.bashrc</em> se carga cuando el terminal es ejecutado sin un login, y resulta que Visual Studio Code carga la terminal integrada con un <em>non-login shell</em>, <a href="https://github.com/Microsoft/vscode/issues/7263">Allow integrated terminal to run as login shell #7263</a>, luego no ejecuta <em>.bash_profile</em> y sí <em>.bashrc</em> (entiendo que esto cambiará en alguna update venidera). ¿Solución? Mover el código de <em>.bash_profile</em> a <em>.bashrc</em> y en <em>.bash_profile </em>incluir código para chequear si existe <em>.bashrc </em>y cargarlo (dándonos así igual si la sesión del terminal es con login o sin login).</p><pre class="brush: bash;">if [ -f ~/.bashrc ]; then
source ~/.bashrc
fi</pre>
<p>Ahora sí, el terminal integrado de Visual Studio Code muestra nuestra personalización.</p>
<p><a href="https://lh3.googleusercontent.com/-a4pHXbvmN5c/WHKoi1BZKzI/AAAAAAAAEZs/dlzZu9iykkI/s1600-h/Screen%252520Shot%2525202017-01-08%252520at%25252020.16.27.png"><img title="Screen Shot 2017-01-08 at 20.16.27" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Screen Shot 2017-01-08 at 20.16.27" src="https://lh3.googleusercontent.com/-XjGwewFYRAU/WHKojh73MBI/AAAAAAAAEZw/4_ak75h0wnE/Screen%252520Shot%2525202017-01-08%252520at%25252020.16.27_thumb.png?imgmax=800" width="326" height="52"></a></p>
<p>Un saludo!</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com1tag:blogger.com,1999:blog-4364815135056214516.post-52813691916718026662016-11-20T15:59:00.001-08:002016-11-20T23:17:45.399-08:00Versionado ASP.NET Web API 2<p>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 <a href="https://github.com/Sebazzz/SDammann.WebApi.Versioning">https://github.com/Sebazzz/SDammann.WebApi.Versioning</a>) al final y en un alarde de <i>“vamos a hacerlo a mano que así aprendemos más y no adquirimos una dependencia”</i> 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 <a href="http://www.hanselman.com/blog/ASPNETCoreRESTfulWebAPIVersioningMadeEasy.aspx">http://www.hanselman.com/blog/ASPNETCoreRESTfulWebAPIVersioningMadeEasy.aspx</a> pero ya es demasiado tarde (al menos por ahora).</p> <p>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 <em>“lo hemos hecho lo mejor que hemos podido”</em></p> <p>Viendo algunos de los distintos tipos de versionado más populares, irá discurriendo el post.</p> <p><b>URI Path</b> <p>Versionar por ruta. El más común, primigenio e intuitivo.</p> <p>Aunque los ejemplos inmediatos no pueden considerarse una buena práctica (de hecho, nadie lo haría así), partiendo de la ruta “<i>/api/Customers”</i> 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.</p><pre class="brush: csharp;">public class Customers2Controller : ApiController
{
// GET api/Customers2
public IEnumerable<Customer2> Get()
{
}
}
</pre>
<p>Ahora, además de <i>“/api/Customers”</i> también tendríamos <i>“/api/Customers2”</i>.</p>
<p>Una segunda opción más recomendable sería utilizar el atributo Route.</p><pre class="brush: csharp;">[Route("api/Customers2")]
public IEnumerable<Customer2> Get()
{
}
</pre>
<p>Otras rutas válidas (y seguro más convenientes que las vistas hasta ahora) serían:</p>
<ul>
<li><i>api/v2/Customers</i>
<li><i>apiv2.example.com/Customers</i> </li></ul>
<p>En la primera el número de versión está en un segmento de la ruta lo más a la izquierda posible.</p>
<p>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 <i>api.example.com</i> apunte a <i>api<última_versión>.example.com</i>.</p>
<p>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 <i>major.minor.patch</i>. Además, si hacemos obligatorio el uso de versión, esto es que no funcione ni <i>api/Customers</i> ni <i>api.example.com</i>, garantizamos que no habrá ninguna sorpresa con los clientes ni dejarán de funcionar cuando subamos de versión.</p>
<p>El versionado por URI Path nos permite cambiar drásticamente nuestra API porque todo lo que sigue a <i>v<versión></i> podría cambiar de una versión a otra. Es decir, podríamos pasar de <i>api/v1/Customers</i>, ya no a <i>api/v2/Customers</i> sino a <i>api/v2/Clientes</i>. 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.</p>
<p>Además, cualquier <i>bookmark</i>, <i>permalink</i> o similar dejará de funcionar y, si no queremos romper nada, tendremos que configurar nuestra aplicación para devolver un <i>302 Found</i> o un <i>301 Moved permanently</i>.</p>
<p>Por otro lado, desde el punto de vista teórico (y poniéndonos la gorra de <i>restafari</i>), 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).</p>
<p>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 <i>constraint namespaces</i> en una ruta tal y cómo sí lo hace MVC, luego tendremos que crear una implementación propia de <i>IHttpControllerSelector</i> que entienda rutas con la plantilla <i>api/{namespace}/{controller}/{id}</i> y busque controladores sólo en el espacio de nombres especificado en <i>{namespace}</i>.</p>
<p>La implementación está aquí <a href="https://blogs.msdn.microsoft.com/webdev/2013/03/07/asp-net-web-api-using-namespaces-to-version-web-apis/">https://blogs.msdn.microsoft.com/webdev/2013/03/07/asp-net-web-api-using-namespaces-to-version-web-apis/</a> 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 <i>api/v1/Customers</i> como <i>api/v2/Customers</i> funcionarían correctamente y no habría ambigüedad en la búsqueda del controlador.</p><pre class="brush: csharp;">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));
</pre>
<p>Cada controlador en su espacio de nombres por versión: </p><pre class="brush: csharp;">namespace ConsoleApplication1.Controllers.v1
{
public class CustomersController : ApiController
}
namespace ConsoleApplication1.Controllers.v2
{
public class CustomersController : ApiController
}
</pre>
<p>Ya lo dice el post de donde se tomó el ejemplo de <i>IHttpControllerSelector</i>, 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 <i>namespace</i> 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 <i>api/v2/Customers</i> quiero que <i>api/v2/Orders</i> siga ejecutando <i>api/v1/Orders</i> y no tener que copiar <i>OrdersControllers</i> al espacio de nombres <i>v2</i> aunque no haya sufrido ningún cambio. Al final del post veremos como lo hemos resuelto en nuestro caso.</p>
<p><b>URI Parameter</b></p>
<p>En este método la versión se especifica como un parámetro de la <i>querystring</i>. Por ejemplo: <i>api/Customers?version=2</i></p>
<p>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.</p>
<p>Para implementar este tipo de versionado en ASP.NET Web API, tomaremos prestada la idea desde <a href="https://www.asp.net/web-api/overview/releases/whats-new-in-aspnet-web-api-21">https://www.asp.net/web-api/overview/releases/whats-new-in-aspnet-web-api-21</a> donde hay un link a un ejemplo para versionar a través de attribute routing <a href="http://aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/RoutingConstraintsSample/ReadMe.txt">http://aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/RoutingConstraintsSample/ReadMe.txt</a></p>
<p>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 <i>api/Customers?version=1</i> y <i>api/Customers?version=2</i>.</p>
<p>Los métodos de acción quedarían así:</p><pre class="brush: csharp;">public class CustomersController : ApiController
{
[VersionedRoute("api/Customers", 1)]
public IEnumerable<Customer> GetCustomers1()
{
return new List<Customer>()
{
};
}
[VersionedRoute("api/Customers", 2)]
public IEnumerable<Customer2> GetCustomers2()
{
}
}
</pre>
<p>Código de <i>VersionedRoute</i> (siempre será el mismo con independencia del tipo de versionado):</p><pre class="brush: csharp;">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) }
};
}
</pre>
<p>Código de <i>VersionConstraint</i>:</p><pre class="brush: csharp;">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;
}
}
</pre>
<p><b>Custom Header</b></p>
<p>Con este método la idea es incluir en la petición una cabecera personalizada del estilo <i>X-Version</i> o similar. El prefijo X- es una convención para cabeceras personalizadas, que no son parte del estándar.</p>
<p>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.</p>
<p>Para su implementación, tendremos que modificar la clase <i>VersionConstraint</i> para soporte adicionalmente el versionado por <i>Custom Header</i>.</p><pre class="brush: csharp;">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;
}
</pre>
<p><b>Content Negotiation</b></p>
<p>Se basa en el uso de la cabecera <i>Accept</i> y un <i>MIME Type</i> personalizado donde se especifica que versión del recurso queremos obtener.</p>
<p>Se presenta en 2 distintas formas:</p>
<ul>
<li><i>application/json; version=1</i>
<li><i>application/vnd.<compañía>.<recurso>+json; version=1</i> </li></ul>
<p>En la primera se usa el tipo MIME estándar y se le agrega un parámetro de versión.</p>
<p>En la segunda se usa un tipo MIME personalizado donde se especifica tanto la versión como el formato deseado.</p>
<p>Por cierto, <i>vnd</i> es de <i>vendor</i> <a href="https://en.wikipedia.org/wiki/Media_type">https://en.wikipedia.org/wiki/Media_type</a></p>
<p>Para implementar ambas, ASP.NET nos ayudará porque cualquier cabecera acepta parámetros y el framework los parsea automáticamente:</p><pre class="brush: csharp;">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;
}
</pre>
<p>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 <i>Customer</i>, las siguientes peticiones devolverán siempre <i>json</i> (porque es el primer <i>MediaTypeFormatter</i> que está registrado en <i>GlobalConfiguration.Configuration.Formatters</i>).</p>
<ul>
<li><i>Accept: application/vnd.example.com+json; version=2</i>
<li><i>Accept: application/vnd.example.com+xml; version=2</i> </li></ul>
<p>Si lo primero que hacemos al empezar un proyecto con Web API es eliminar el <i>XmlFormatter</i> 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 <i>MediaTypeFormatter</i> para cada tipo y para cada representación. Un ejemplo completo se puede ver en <a href="http://robertgaut.com/Blog/2007/Four-Ways-to-Version-Your-MVC-Web-API">http://robertgaut.com/Blog/2007/Four-Ways-to-Version-Your-MVC-Web-API</a></p>
<p>Como muestra el post, lo hay que hacer es crear un par de <i>MediaTypeFormatter</i> (uno para <i>json</i> y otro para <i>xml</i>) 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:</p><pre class="brush: csharp;">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();
}
}
</pre>
<p>Y el registro:</p><pre class="brush: csharp;">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")));
</pre>
<p>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.</p>
<p>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:</p>
<ul>
<li>Establecer el actual número de versión (el valor más alto, en mi caso el endpoint más digievolucionado).
<ul>
<li>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. </li></ul>
<li>Buscar por reflexión rutas versionadas y calcular dinámicamente a que versiones de la misma ruta puede hacer fallback. </li></ul><pre class="brush: csharp;">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));
}
}
</pre>
<p>FallbackRoute es una clase que simplemente guardar una ruta y a que versiones atiende:</p>
<pre class="brush: csharp;">
internal class FallbackRoute
{
public FallbackRoute(string routeTemplate, int allowedVersion)
{
RouteTemplate = routeTemplate;
AllowedVersion = allowedVersion;
FallbackVersions = new List<int>();
}
public string RouteTemplate { get; }
public int AllowedVersion { get; }
public IEnumerable<int> FallbackVersions { get; }
public bool HasFallbackVersion(int version)
{
return FallbackVersions.Contains(version);
}
public void AddFallbackVersion(int version)
{
((IList<int>)FallbackVersions).Add(version);
}
}
</pre>
<p>Y en VersionedConstraint hay que cambiar el código del método Match para que sepa a qué rutas de fallback responderá:</p><pre class="brush: csharp;">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));
}
</pre>
<p>Con estos cambios y asumiendo que CurrentVersion = 3, funcionarán las siguientes rutas:</p>
<ul>
<li>VersionedRoute(“api/Customers”, 1) responderá a la versión 1.
<li>VersionedRoute(“api/Customers”, 3) responderá a la versiones 2 y 3.
<li>VersionedRoute(“api/Orders”, 2) responderá a la versiones 2 y 1.
<li>VersionedRoute(“api/Orders”) responderá a la versión 3. </li></ul>
<p>El cómo organizar el código en <i>namespaces</i> 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.</p>
<p>Si por algún motivo te parece una buena solución, te recomendaría usar el código de github <a href="https://github.com/panicoenlaxbox/WebApiVersioning">https://github.com/panicoenlaxbox/WebApiVersioning</a> donde, seguro, estará la versión más actualizada y que funciona.</p>
<p>Un saludo!</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com2tag:blogger.com,1999:blog-4364815135056214516.post-9451449198192500422016-10-04T02:07:00.001-07:002016-10-04T02:09:47.440-07:00Dejar de seguir la pista a un fichero en git<p>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.</p>
<p>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.</p>
<p>Los comandos necesarios serían algo así:</p>
<pre class="brush: plain;">
git rm --cached your_file
git add .
git commit -m "Eliminado fichero de git"
</pre>
<p>A continuación editamos el fichero .gitignore para agregar el fichero… y ya está!</p>
<p>Un saludo!</p>
Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com1tag:blogger.com,1999:blog-4364815135056214516.post-70117459261685356382016-09-06T01:47:00.000-07:002016-09-25T07:34:27.702-07:00Restaurar y renombrar una base de datos<p>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.</p>
<p>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.</p>
<p>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.</p>
<pre class="brush: sql;">
RESTORE FILELISTONLY FROM DISK = 'C:\Users\sergio.leon\Sergio.bak'
</pre>
<p>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):</p>
<pre class="brush: sql;">
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'
</pre>
<p>
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.</p>
<p>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:</p>
<pre class="brush: sql;">
ALTER DATABASE [panicoenlaxbox] MODIFY FILE (NAME=N'Sergio', NEWNAME=N'panicoenlaxbox')
ALTER DATABASE [panicoenlaxbox] MODIFY FILE (NAME=N'Sergio_log', NEWNAME=N'panicoenlaxbox_log')
</pre>
<p>Otro problema común (al menos en mi caso) es tener que renombrar una base de datos.</p>
<p>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.</p>
<pre class="brush: sql;">
sp_renamedb 'Sergio', 'SergioLeon'
</pre>
<p>Si hay conexiones abiertas no podrás hacer esto, con el siguiente script matarás estas conexiones:</p>
<pre class="brush: sql;">
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;
</pre>
<p>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.</p>
<pre class="brush: sql;">
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
</pre>
<p>Un saludo!</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com1tag:blogger.com,1999:blog-4364815135056214516.post-42790897634474545852016-07-07T03:01:00.001-07:002016-07-07T03:01:31.342-07:00Publicar WebJob con WebDeploy<p>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.</p>
<p>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.”</p>
<p>Está claro, el WebJob debe ser parte del proyecto.</p>
<p>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…).</p>
<p>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.</p>
<p>En mi caso he optado por agregar código al fichero .pubxml (publicación).</p>
<p>Primero, restaurar los paquetes de Node, invocando un target antes de la publicación <a href="http://stackoverflow.com/a/12920499">http://stackoverflow.com/a/12920499</a></p>
<pre class="brush: xml;">
<PropertyGroup>
<PipelineDependsOn>
CustomBeforePublish;
$(PipelineDependsOn);
</PipelineDependsOn>
</PropertyGroup>
<Target Name="CustomBeforePublish">
<Exec Command="npm install" WorkingDirectory="$(SolutionDir)MyApplication\App_Data\jobs\triggered\MyWebJob"/>
</Target>
</pre>
<p>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.</p>
<p>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.</p>
<p>Segundo (y porque la carpeta node_modules no está incluida en el .csproj), copiar estos ficheros en el paquete de publicación <a href="http://www.hackthedot.dk/2012/10/adding-extra-files-to-deployment.html">http://www.hackthedot.dk/2012/10/adding-extra-files-to-deployment.html</a></p>
<pre class="brush: xml;">
<Target Name="IncludeNodeModules">
<ItemGroup>
<NodeFiles Include="$(SolutionDir)MyApplication\App_Data\jobs\triggered\MyWebJob\node_modules\**\*.*" />
<FilesForPackagingFromProject Include="%(NodeFiles.Identity)">
<DestinationRelativePath>App_Data\jobs\triggered\MyWebJob\node_modules\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
</FilesForPackagingFromProject>
</ItemGroup>
</Target>
<PropertyGroup>
<CopyAllFilesToSingleFolderForMsdeployDependsOn>
IncludeNodeModules;
$(CopyAllFilesToSingleFolderForPackageDependsOn);
</CopyAllFilesToSingleFolderForMsdeployDependsOn>
</PropertyGroup>
</pre>
<p>Con esto se ha salvado la papeleta, ahora el WebJob forma parte del proyecto y la publicación lo tiene en cuenta.</p>
<p>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 <a href="https://github.com/projectkudu/kudu/wiki/Web-jobs#configuration-settings">https://github.com/projectkudu/kudu/wiki/Web-jobs#configuration-settings</a></p>
<p>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.</p>
<p>Un saludo!</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com0tag:blogger.com,1999:blog-4364815135056214516.post-88837351898302683472016-03-01T09:55:00.001-08:002016-03-01T10:04:12.176-08:00Binding de listas en ASP.NET MVC<p>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).</p>
<p>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<string> foo, podríamos llamarlo con <i>?foo=Bar&foo=Baz</i> y todo funcionará a la perfección.</p>
<p>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.</p>
<p>Por ejemplo, si esperamos recibir un objeto Person:</p>
<pre class="brush: c#;">
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; }
}
</pre>
<p>La url que deberíamos enviar por GET sería:</p>
<pre class="brush: plain;">
name=John
&addresses[0].city=Madrid
&addresses[0].country=Spain
&addresses[1].city=New York
&addresses[1].country=USA</pre>
<p>Fíjate que la norma es propiedad[índice].propiedad, por ejemplo addresses[0].country</p>
<p>Lo más sencillo es utilizar los helpers de Html en la vista para que los atributos name se generen acorde a esta norma.</p>
<p>Sin embargo, ¿Cómo enviamos un triste objeto Javascript cumpliendo esta norma?</p>
<p>En mi caso usaré jQuery.</p>
<p>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).</p>
<pre class="brush: js;"> 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"
});
</pre>
<p>Sin embargo, si queremos enviar ese mismo objeto por GET no funcionará.</p>
<p>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:</p>
<pre class="brush: js;">$.param({
name: "John",
addresses: [
{ city: "Madrid", country: "Spain" },
{ city: "New York", country: "USA" }
]
});
</pre>
<pre class="brush: plain;">
name=John
&addresses[0][city]=Madrid
&addresses[0][country]=Spain
&addresses[1][city]=New York
&addresses[1][country]=USA</pre>
<p>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.</p>
<p>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).</p>
<pre class="brush: js;"> 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;
})();
</pre>
<p>Ahora una llamada con:</p>
<pre class="brush: js;">
serialize({
name: "John",
addresses: [
{ city: "Madrid", country: "Spain" },
{ city: "New York", country: "USA" }
]
});
</pre>
<p> 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.</p>
<pre class="brush: plain;">
name=John
&addresses[0].city=Madrid
&addresses[0].country=Spain
&addresses[1].city=New York
&addresses[1].country=USA</pre>
<p>Un saludo!</p>Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com2tag:blogger.com,1999:blog-4364815135056214516.post-23136951049292629232015-11-26T03:30:00.001-08:002015-11-26T12:19:08.331-08:00Contratación ¿En qué me estoy equivocando?<p>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. <p>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%). <p>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. <p>Las primeras preguntas que tuvimos resolver fueron qué perfil buscar y que sueldo ofrecer. <p>En cuando al perfil (y después de mucho divagar), apostamos por un <strong>full-stack</strong> centrado en tecnologías <strong>.NET</strong>. Es decir, un back-end (<strong>ASP.NET MVC, Web API, Entity Framework</strong>), pero que se defienda razonablemente bien con <strong>Javascript</strong> y <strong>jQuery</strong>. 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). <p>En relación al sueldo, hemos determinado que podemos llegar hasta <strong>33K</strong>… 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. <p>Además, tenemos un buen horario (que por ahora más o menos cumplimos), de <strong>lunes a jueves de 8 a 17:00 y los viernes de 8 a 14:00</strong>. 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. <p>Y con estos mimbres nos lanzamos al mercado laboral seguros de encontrar a un nuevo compañero… <p>Y a partir de aquí empieza nuestro vía crucis. <p>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…). <p>Para la preselección de candidatos hemos optado por una empresa de <i>recruiting</i> (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 <i>¿Qué preguntar cuando entrevistas a un candidato?</i> 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. <p>Como al principio invertimos mucho tiempo en ver gente que tenía <strike>importantes</strike> suficientes carencias técnicas llegamos a la conclusión de que el filtro previo de la empresa de <i>recruiting</i> 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). <p>Esa primera entrevista técnica es la siguiente: <ul> <li>POO <ul> <li>Diferencias entre una clase abstracta y una interfaz</li></ul> <li>ASP.NET MVC <ul> <li>Diferencia entre @Html.Partial y @Html.Action <li>Diferencia entre @helper y @functions</li></ul> <li>ASP.NET WebAPI <ul> <li>¿Para qué sirve [HttpGet] y [HttpPost]? <li>¿Para qué sirve [FromUri] y [FromBody]?</li></ul> <li>Entity Framework <ul> <li>Diferencia entre Database First y Code First <li>¿Qué hace DbSet.Find? <li>Diferencia entre IEnumerable e IQueryable <li>Lazy loading vs Eager loading vs Explicit loading</li></ul> <li>Javascript/jQuery <ul> <li>Hablando de eventos ¿Qué es la delegación? <li>¿Qué es un closure? <li>¿Qué es una promise?</li></ul></li></ul> <p>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. <p>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? <p>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. <p>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: <p>“¿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?” <p>Y ya si la conversación va viento en popa le pregunto: <p>“¡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!” <p>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 <strong>¿En qué me estoy equivocando?</strong> <p>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 <i>softskills</i>, 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 <em>“Top interview questions about [inserta aquí tecnología/lenguaje]”</em>. <p>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 <i>promise</i> o <i>closure</i>. <p>En este punto queda claro que algo está fallando. ¿Posibles reflexiones? Yo hago las siguientes. <p>¿Tendré una visión distorsionada del mercado laboral y por 33K tengo que contratar a un <i>padawan</i> en vez de a un señor programador? Puede ser, yo creo que no, que es un sueldo majo, pero ya dudo de todo… <p>¿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? <p>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… <p>¿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 <a href="http://mscoders-madrid.tumblr.com/">MsCoders</a>, <a href="https://madriddotnet.wordpress.com/">Madrid.NET</a>, <a href="http://www.formaciontajamar.com/">Formación Tajamar</a> o similar (y participar es “ir”, “oír”, no significa ser el ponente). <p>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é. <p><em>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 </em><a title="https://betabeers.com/post/programador-fullstack-net-2379/" href="https://betabeers.com/post/programador-fullstack-net-2379/"><em>https://betabeers.com/post/programador-fullstack-net-2379/</em></a> <p>Un saludo!</p> Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com39tag:blogger.com,1999:blog-4364815135056214516.post-79565584399560311852015-11-24T10:05:00.001-08:002015-11-24T14:12:23.457-08:00Crear índices en EF con Fluent API<p>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.</p> <p>Partiendo de una entidad muy sencilla como la siguiente:</p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff"><font style="font-size: 9.5pt">class</font></font></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#2b91af">Customer</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font style="font-size: 9.5pt" color="#000000">{</font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">public</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">int</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> Id { </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">get</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000">; </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">set</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">; }</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">public</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">string</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> Name { </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">get</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000">; </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">set</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">; }</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">public</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">string</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> Country { </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">get</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000">; </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">set</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">; }</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">public</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">DateTime</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> CreatedDate { </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">get</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000">; </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">set</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">; }</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 8pt; line-height: 12pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font style="font-size: 9.5pt" color="#000000">}</font></font></span></p> <p>Hacer un índice único por el campo Name sería asi:</p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font face="Consolas"><font style="font-size: 9pt" color="#000000">modelBuilder</font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9pt"> </font></span><font style="font-size: 9pt">.Entity<</font></font></span><font style="font-size: 9pt"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#2b91af">Customer</font></span></font><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font style="font-size: 9pt" color="#000000">>()</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9pt"> </font></span><font style="font-size: 9pt">.Property(t => t.Name)</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9pt"> </font></span><font style="font-size: 9pt">.HasMaxLength(250)</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9pt"> </font></span><font style="font-size: 9pt">.HasColumnAnnotation(</font></font></span><font style="font-size: 9pt"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#2b91af">IndexAnnotation</font></span></font><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font style="font-size: 9pt" color="#000000">.AnnotationName,</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 8pt; line-height: 12pt"><font face="Consolas"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white; mso-ansi-language: en-us"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9pt"> </font></font></span></span><font style="font-size: 9pt"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white; mso-ansi-language: en-us"><font color="#0000ff">new</font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white; mso-ansi-language: en-us"><font color="#000000"> </font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white; mso-ansi-language: en-us"><font color="#2b91af">IndexAnnotation</font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white; mso-ansi-language: en-us"><font color="#000000">(</font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white; mso-ansi-language: en-us"><font color="#0000ff">new</font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white; mso-ansi-language: en-us"><font color="#000000"> </font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white; mso-ansi-language: en-us"><font color="#2b91af">IndexAttribute</font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white; mso-ansi-language: en-us"><font color="#000000">() { IsUnique = </font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white; mso-ansi-language: en-us"><font color="#0000ff">true</font></span></font><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white; mso-ansi-language: en-us"><font style="font-size: 9pt" color="#000000"> }));</font></span></font><span lang="EN-US" style="font-family: ; color: ; line-height: 10pt; mso-ansi-language: en-us"></span></p> <p>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:</p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#0000ff"><font style="font-size: 9.5pt">var</font></font></span><font style="font-size: 9.5pt"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#000000"> customer = modelBuilder.Entity<</font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#2b91af">Customer</font></span></font><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font style="font-size: 9.5pt" color="#000000">>();</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#0000ff"><font style="font-size: 9.5pt">const</font></font></span><font style="font-size: 9.5pt"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#000000"> </font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#0000ff">string</font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#000000"> indexName = </font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#a31515">"IX_Customers"</font></span></font><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font style="font-size: 9.5pt" color="#000000">;</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font face="Consolas"><font style="font-size: 9.5pt" color="#000000">customer</font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">.Property(t => t.Name)</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">.HasMaxLength(250)</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">.HasColumnAnnotation(</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#2b91af">IndexAnnotation</font></span></font><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font style="font-size: 9.5pt" color="#000000">.AnnotationName,</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#0000ff">new</font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#000000"> </font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#2b91af">IndexAnnotation</font></span></font><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font style="font-size: 9.5pt" color="#000000">(</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#0000ff">new</font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#000000"> </font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#2b91af">IndexAttribute</font></span></font><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font style="font-size: 9.5pt" color="#000000">(indexName, 1)</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">{</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">IsUnique = </font></font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font style="font-size: 9.5pt" color="#0000ff">true</font></span></font><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt"><span style="mso-spacerun: yes"> </span>}));</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font face="Consolas"><font style="font-size: 9.5pt" color="#000000">customer</font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">.Property(t => t.Country)</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">.HasMaxLength(250)</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">.HasColumnAnnotation(</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#2b91af">IndexAnnotation</font></span></font><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font style="font-size: 9.5pt" color="#000000">.AnnotationName,</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#0000ff">new</font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#000000"> </font></span><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font color="#2b91af">IndexAnnotation</font></span></font><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><font style="font-size: 9.5pt" color="#000000">(</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span lang="EN-US" style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-ansi-language: en-us"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white"><font color="#0000ff">new</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white"><font color="#2b91af">IndexAttribute</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white"><font style="font-size: 9.5pt" color="#000000">(indexName, 2)</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">{</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">IsUnique = </font></font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white"><font style="font-size: 9.5pt" color="#0000ff">true</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white"></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 8pt; line-height: 12pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">}));</font></font></font></span><span lang="EN-US" style="font-family: ; color: ; line-height: 10pt; mso-ansi-language: en-us"></span></p> <p>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.</p> <p>Originalmente el post llevaba hasta aquí, pero el bueno de <a href="https://twitter.com/gulnor">@gulnor</a> me incito <a href="https://twitter.com/gulnor/status/669223770423472128">me incitó de mala manera</a> a dar una solución y no sólo exponer un problema, y claro, después de leer su reciente y reflexivo post <a href="http://blog.koalite.com/2015/11/codigo-abierto-ecosistema-cerrado/">Código abierto, ecosistema cerrado</a>, 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) :)</p> <p>La idea es poder crear un índice sin todo el ruido que genera el código anterior, algo asi:</p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><font style="font-size: 9.5pt">modelBuilder.Entity<</font></font></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">Customer</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000">>().Index(p => p.Name, </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">true</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000">, </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#a31515">"IX_Customers"</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">, 1);</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><font style="font-size: 9.5pt">modelBuilder.Entity<</font></font></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">Customer</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000">>().Index(p => p.Country, </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">true</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000">, </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#a31515">"IX_Customers"</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">, 2);</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><font style="font-size: 9.5pt">modelBuilder.Entity<</font></font></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">Customer</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">>().Index(p => p.CreatedDate);</font></span></font></p> <p>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):</p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff"><font style="font-size: 9.5pt">static</font></font></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">class</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#2b91af">ModelBuilderExtensions</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font style="font-size: 9.5pt" color="#000000">{</font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">private</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">static</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">void</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000"> CreateIndex(</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">PrimitivePropertyConfiguration</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000"> property, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">bool</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> isUnique = </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">false</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">string</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> name = </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">null</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">int</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000"> position = 1)</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">{</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">var</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> indexAttribute = </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">new</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">IndexAttribute</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">();</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">if</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> (!</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">string</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">.IsNullOrWhiteSpace(name))</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">{</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">indexAttribute = </font></font></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">new</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">IndexAttribute</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">(</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">name, </font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">position <= 0 ? 1 : position);</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">}</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">indexAttribute.IsUnique = isUnique;</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">property.HasColumnAnnotation(</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">IndexAnnotation</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">.AnnotationName, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">new</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">IndexAnnotation</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">(indexAttribute));</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">}</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font style="font-size: 9.5pt" color="#000000"> </font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">public</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">static</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">void</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> Index<</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000">, </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T2</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">>(</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt"><span style="mso-spacerun: yes"> </span></font></font></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">this</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">EntityTypeConfiguration</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">> entity, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">Expression</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">Func</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000">, </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T2</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">>> propertyExpression, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">bool</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> isUnique = </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">false</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">string</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> name = </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">null</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">int</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> position = 1) </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">where</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> : </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">class</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">where</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T2</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> : </font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#0000ff">struct</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">{</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">var</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000"> property = entity.Property(propertyExpression);</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">CreateIndex(property, isUnique, name, position);</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">}</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font style="font-size: 9.5pt" color="#000000"> </font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">public</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">static</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">void</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> Index<</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000">, </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T2</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">>(</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">this</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">EntityTypeConfiguration</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">> entity, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">Expression</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">Func</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000">, </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T2</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">?>> propertyExpression, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt"><span style="mso-spacerun: yes"> </span></font></font></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">bool</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> isUnique = </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">false</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">string</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> name = </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">null</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">int</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> position = 1) </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">where</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> : </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">class</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">where</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T2</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> : </font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#0000ff">struct</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">{</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">var</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000"> property = entity.Property(propertyExpression);</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">CreateIndex(property, isUnique, name, position);</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">}</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font style="font-size: 9.5pt" color="#000000"> </font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">public</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">static</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">void</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> Index<</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">>(</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">this</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">EntityTypeConfiguration</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">> entity, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">Expression</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">Func</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"><</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000">, </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">string</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">>> propertyExpression, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">bool</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> isUnique = </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">false</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">string</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> name = </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">null</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000">, </font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">int</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> position = 1) </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">where</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> </font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#2b91af">T</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#000000"> : </font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#0000ff">class</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">{</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><span style="mso-spacerun: yes"><font color="#000000"><font style="font-size: 9.5pt"> </font></font></span></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font color="#0000ff">var</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font style="font-size: 9.5pt" color="#000000"> property = entity.Property(propertyExpression);</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">CreateIndex(property, isUnique, name, position);</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font color="#000000"><span style="mso-spacerun: yes"><font style="font-size: 9.5pt"> </font></span><font style="font-size: 9.5pt">}</font></font></font></span></p> <p class="MsoNormal" style="margin: 0cm 0cm 8pt; line-height: 12pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white; mso-bidi-font-family: consolas"><font face="Consolas"><font style="font-size: 9.5pt" color="#000000">}</font></font></span></p> <p>Ahora sí, post cerrado!</p> <p>Un saludo!</p> Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com0tag:blogger.com,1999:blog-4364815135056214516.post-84168527646638192462015-11-06T15:58:00.001-08:002016-09-02T01:27:51.313-07:00Code First Migrations: Borrón y cuenta nueva<p>Cuando trabajas con Code First es muy habitual (por no decir casi obligatorio si no buscas alguna alternativa) el trabajar con <a href="http://panicoenlaxbox.blogspot.com.es/2013/05/rompiendo-el-hielo-con-code-first.html">migraciones basadas en código</a>.</p> <p>Con el paso del tiempo, te pasará que la carpeta Migrations tendrá una cantidad ingente de código que refleja el cómo ha evolucionado tu modelo. Si te resulta necesario mantener este código para poder hacer un hipotético downgrade de tu aplicación, no sigas leyendo, este post no es para ti. Sin embargo, si tienes la certeza de que tu última versión publicada es válida y no vas a querer nunca revertir los cambios, puede ser una buena idea el desprenderte de esa base de código que está ahi, pero no te va a servir para nada… bueno sí, para que VS vaya más lento y ReSharper tenga más trabajo cuando actives <a href="https://www.jetbrains.com/resharper/help/Code_Analysis__Solution-Wide_Analysis.html">Solution-Wide Analysis</a>.</p> <p>Decididos ya a soltrar lastre ¿Cómo elimino todas las migraciones de __MigrationHistory y hago coincidir mi modelo con el esquema actual de la base de datos?</p> <p>El proceso a seguir es el siguiente:</p> <ul> <li>Borrar la tabla __MigrationHistory <li>Borrar la carpeta Migrations de VS <li>Ejecutar el comando Enable-Migrations <li>Ejecutar el comando Add-Migration InitialCreate <ul> <li>Que el nombre de la migración sea InitialCreate es sólo una convención, podría ser cualquier otro.</li></ul> <li>Comentar el código del método Up de la migracion <li>Ejecutar el comando Update-Database <ul> <li>Creará la tabla __MigrationHistory</li></ul> <li>Descomentar el código del método Up de la migración <ul> <li>Así al llamar a Update-Database siguientes veces y si la base de datos no existe, se creará una nueva con todos los objetos que se capturaron en este primer snapshot.</li></ul></li></ul> <p>Después de esto tendremos una nueva tabla __MigrationHistory con una sola migración que coincidira con nuestro modelo.</p> <p>Lógicamente, para llevar a cabo esta operación primero deberíamos tener en cuenta si hemos incluido código personalizado en el método Up de alguna migración. En caso afirmativo, deberemos analizar ese código de forma particular y ver si sigue siendo necesario en nuestro nuevo e impoluto escenario.</p> <p>Un saludo!</p> Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com3tag:blogger.com,1999:blog-4364815135056214516.post-72325276380225185512015-09-24T12:42:00.001-07:002015-09-24T12:42:49.947-07:00Visualizar el estado de las entidades en Entity Framework<p>Si programas con Entity Framework, tarde o temprano tendrás algún problema al querer saber cuál es el estado de todas las entidades que está siguiendo el contexto. </p> <p>Claro está que puedes utilizar el método Entry o incluso saber si una propiedad está modificado con un código similar al siguiente:</p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white"><font color="#2b91af"><font style="font-size: 9.5pt">DbEntityEntry</font></font></span><font style="font-size: 9.5pt"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white"><font color="#000000"><</font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white"><font color="#2b91af">Customer</font></span></font><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white"><font style="font-size: 9.5pt" color="#000000">> entry = context.Entry(customer);</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 0pt; line-height: normal; text-autospace: ; mso-layout-grid-align: none"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white"><font color="#2b91af"><font style="font-size: 9.5pt">Debug</font></font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; mso-highlight: white"><font style="font-size: 9.5pt" color="#000000">.WriteLine(entry.State);</font></span></font></p> <p class="MsoNormal" style="margin: 0cm 0cm 8pt; line-height: 12pt"><font face="Consolas"><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white"><font color="#2b91af"><font style="font-size: 9.5pt">Debug</font></font></span><span style="font-family: ; background-image: none; background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; color: ; line-height: 10pt; mso-highlight: white"><font style="font-size: 9.5pt" color="#000000">.WriteLine(entry.Property(p => p.Name).IsModified);</font></span></font></p> <p>En cualquier caso, ver el estado de todas las entidades y que propiedades han sido modificadas (si es que la entidad ha sido modificada) se puede convertir en una tarea ardua y no trivial.</p> <p>Para estos casos (yo directamente es un <a href="http://www.scip.be/index.php?Page=ArticlesNET16#Dump">código sin el que no puedo vivir</a>) encontré un día un método extensor de ObjectStateManager (DbContext lo sigue utilizando bajo cuerda) que vuelca toda la información de seguimiento en HTML, bravo!!</p> <p>Con este nuevo y awesómico método, no hay sesion de debugging que se resista… porque sí, yo depuro y mucho, lo reconozco :)</p> <p><a href="http://lh3.googleusercontent.com/-iAVxrrrVwxw/VgRSL-4KBPI/AAAAAAAAEA0/FBXpl3LiiG8/s1600-h/image%25255B5%25255D.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; margin: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://lh3.googleusercontent.com/-ZLyzyqjtIks/VgRSMl4_iaI/AAAAAAAAEA4/T3snstGEX6s/image_thumb%25255B1%25255D.png?imgmax=800" width="244" height="75"></a></p> <p><a href="http://lh3.googleusercontent.com/-qZc0ZUUDmvw/VgRSNJJB76I/AAAAAAAAEBA/lozHHuwC25s/s1600-h/image%25255B2%25255D.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://lh3.googleusercontent.com/-p79khwOp5KM/VgRSOLSXx3I/AAAAAAAAEBI/QjAMUW7sIpA/image_thumb.png?imgmax=800" width="240" height="244"></a></p> <p>He subido un <a href="https://github.com/panicoenlaxbox/DbContextChangeTrackerVisualizer">proyecto muy sencillo en github</a> donde se puede ver en un periquete como funciona el asunto.</p> <p>Un saludo!</p> Sergio Leónhttp://www.blogger.com/profile/07959547392771610349noreply@blogger.com0