Este artículo es una traducción de "Lessons learned after migrating 25+ projects to .NET Core" de Thomas Ardal, CEO de elmah.io, con su permiso expreso. elmah.io es el principal servicio del mercado para monitorización y logging de aplicaciones .NET.
Hace poco terminamos una de las mayores tareas de refactorización que hemos hecho en elmah.io: migrar todo a .NET Core. elmah.io consta actualmente de 5 aplicaciones web y 57 funciones de Azure repartidas en aproximadamente 25 Function Apps. En este post, compartiré algunas de las lecciones que hemos aprendido mientras llevábamos a cabo esta tarea.
Vamos al grano. He dividido este post en tres categorías. Una sobre los problemas de migración de .NET Framework a .NET Core en general. Otra específicamente sobre la migración de ASP.NET a ASP.NET Core. Y la tercera sobre la migración de las Azure Functions a la versión más reciente. Siéntete libre de sumergirte en el tema que más te interese. Algunos de los contenidos están divididos en temas más específicos que nos fuimos encontrando, mientras que otros son más bien una descripción textual de dónde nos encontramos actualmente.
.NET Core
Para empezar con buenas noticias, esperaba muchos más problemas al migrar el código de .NET Framework a .NET Core. Cuando empezamos a experimentar con la migración, .NET Core estaba en la versión 1.x y faltaban aún muchas características de .NET Framework "clásico". Desde la versión 2.2 en adelante, no recuerdo haber echado de menos nada. Si saltas directamente a la última versión estable de .NET Core (no veo motivos por lo que no debieses hacerlo), hay muchas probabilidades de que tu código se compile y funcione sin necesidad de realizar ningún cambio.
Hay que estar atento a los niveles de soporte
Una cosa que debes tener en cuenta cuando saltes de .NET "clásico" a .NET Core, es un despliegue más rápido de las nuevas versiones. Eso incluye también intervalos de soporte más cortos. Con .NET "clásico", 10 años de soporte eran lo más normal, cuando ahora los 3 años de soporte de .NET Core son el periodo que se ofrece. Además, cuando se elige la versión de .NET Core con la que quieres compilar, hay que mirar el nivel de soporte que te proporciona cada versión. Microsoft marca ciertas versiones con soporte de largo plazo (LTS) que es alrededor de 3 años, mientras que otras son versiones intermedias. Estables, pero aún así versiones con un período de soporte más corto. En general, estos cambios requieren que actualices la versión .NET Core más a menudo de lo que estás acostumbrado o asumir que vas a ejecutar tus aplicaciones en una versión del framework ya no soportada.
Aquí tienes una buena visión general de las diferentes versiones y sus niveles de soporte: Política de soporte de .NET Core.
ASP.NET Core
Migrar nuestros sitios web ASP.NET MVC a ASP.NET Core ha sido la mayor tarea de todas. No quiero asustarte para que no migres y, de hecho, la mayor parte del tiempo lo pasamos migrando algunos viejos frameworks de autenticación y haciendo tareas similares no relacionadas con .NET Core. La parte MVC en ASP.NET Core funciona de manera muy parecida a la antigua y se llega muy lejos haciendo buscar y reemplazar global con algunos patrones. En las siguientes secciones, he enumerado varios problemas con los que nos encontramos durante la migración.
Cómo hacer la actualización
La ruta de actualización no es exactamente directa. Puede que existan algunas herramientas que te ayudan con ello, pero al final acabé migrando todo a mano. Para cada sitio web, hice una copia de todo su repositorio. Después borré todos los archivos de la carpeta de trabajo y creé un proyecto ASP.NET Core MVC nuevo. Luego porté cada cosa una por una. Empezando por copiar los controladores, vistas y modelos y haciendo uso de algunos patrones globales de búsqueda-reemplazo para lograr que compilase. Casi todo fue distinto a partir de ahí. Estábamos usando un viejo marco de trabajo de autenticación, y lo portamos a la característica de autenticación que trae de serie .NET Core (no a Identity). Lo mismo con Swagger, que requiere un nuevo paquete NuGet para .NET Core. etc. Nos llevó mucho tiempo migrar con seguridad el sitio web principal.
Entendiendo el middleware
Como seguramente ya sabrás, muchas de las características de ASP.NET Core están construidas sobre middlewares. El concepto de middleware no existe en ASP.NET y por lo tanto es algo que debes aprender. La forma en que configuras los middlewares y especialmente el orden en el que los instalas es algo que nos ha causado un cierto dolor de cabeza. Mi recomendación sería que estudies muy a fondo la documentación de Microsoft para este caso. Además, Andrew Lock escribió una larga serie de artículos de alta calidad sobre middleware en los que recomiendo a todo el mundo que se sumerja: https://andrewlock.net/tag/middleware/
Newtonsoft.Json vs System.Text.Json
Hasta ASP.NET Core 3.0, la serialización y deserialización de JSON se llevaba a cabo con el extremadamente popular paquete Newtonsoft.Json
. Microsoft decidió lanzar su propio paquete para lo mismo en ASP.NET Core 3.0 y versiones posteriores, lo que causó algunos problemas al actualizar de la versión 2.2. a la 3.1.
System.Text.Json
parece que ya es una buena implementación y, después de algunas pruebas, decidimos ir adelante con ello (es la opción por defecto en cualquier caso). Rápidamente descubrimos que Newtonsoft.Json
y System.Text.Json
no son compatibles en la manera en la que serializan y deserializan los objetos C#. Como nuestro cliente usa Newtonsoft.Json
para serializar JSON, experimentamos algunos escenarios en los que el cliente generaba JSON que no podía ser deserializado a C# en el servidor. Mi recomendación, en caso de que estés usando Newtonsoft.Json
en el cliente, es usar también Newtonsoft.Json
en el servidor:
services
.AddControllersWithViews()
.AddNewtonsoftJson();
Después de llamar a AddControllersWithViews
o cualquier otro método que llames para configurar los endpoints, puedes llamar al método AddNewtonsoftJson
para que ASP.NET Core utilice ese paquete.
El cambio necesita de un paquete NuGet adicional:
Install-Package Microsoft.AspNetCore.Mvc.NewtonsoftJson
Mayúsculas y minúsculas en JSON
Un problema al que nos enfrentamos en nuestra aplicación principal fue el uso de mayúsculas y minúsculas al serializar JSON. Cuando la acción de un controlador de ASP.NET Web API devuelve JSON:
return Json(new { Hello = "World" });
El JSON devuelto usa "pascal case" (o sea, todas las primeras letras de las palabras en mayúsculas):
{"Hello":"World"}
Pero cuando se devuelve lo mismo desde ASP.NET Core, el JSON devuelto usa "camel case" (la primera letra de la primera palabra en minúsculas, el resto en mayúsculas):
{"hello":"World"}
Si tú, al igual que nosotros, ya tienes un cliente basado en JavaScript que espera que "pascal case" para los objetos serializados desde C#, puede cambiar el "casing" con esta opción:
services
.AddControllersWithViews()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});
Compilación en tiempo de ejecución de Razor
Aunque portar las vistas de Razor de ASP.NET MVC a ASP.NET Core no requirió mucho trabajo, no tenía claro que las vistas Razor no se compilaban en tiempo de ejecución en el caso de ASP.NET Core. Escribí un artículo en el blog sobre ello: Agregar compilación en tiempo de ejecución para Razor al desarrollar con ASP.NET Core. En resumen: es necesario marcar la opción "Habilitar la compilación en tiempo de ejecución" de Razor al crear el proyecto, o bien instalar el paquete NuGet Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation
y habilitar la compilación en tiempo de ejecución en Startup.cs
:
services
.AddControllersWithViews()
.AddRazorRuntimeCompilation();
Los archivos Web.config siguen siendo válidos
La mayoría de los documentos y blogs mencionan que el archivo web.config
se sustituyó por el archivo appsettings.json
en ASP.NET Core. Aunque esto es cierto, todavía necesitamos el archivo web.config
para implementar en IIS algunos de los encabezados de seguridad como se describe en este post: Guía de cabeceras de seguridad de ASP.NET Core.
Aquí hay un ejemplo del archivo web.config
que utilizamos para eliminar las cabeceras Server
y X-Powered-By
que se ponen automáticamente en las respuestas HTTP cuando la aplicación se hospeda en IIS:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<security>
<requestFiltering removeServerHeader="true" />
</security>
<httpProtocol>
<customHeaders>
<remove name="X-Powered-By" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
Soporte de Azure
Si tu aplicación se ejecuta en Azure (como las nuestras) debes averiguar qué versiones de .NET Core soporta cada región de Azure. Cuando se lanzan nuevas versiones de .NET Core, las regiones de Azure se actualizan en un período que puede llevar de semanas a incluso meses. Antes de actualizar, debes comprobar si tu región es compatible con la versión a la que te estás actualizando. La mejor forma de averiguarlo la tienes en el .NET Core en App Service Dashboard.
Bundling y minificación
El bundling y la minificación era una de las cosas que, creo que funcionaban muy bien en ASP.NET MVC. En ASP.NET Core tienes bastantes opciones diferentes para hacer lo mismo, lo cual es bueno, pero también un poco confuso cuando vienes directamente de ASP.NET MVC. Microsoft tiene un buen documento sobre el tema aquí: Agrupar y minimizar los activos estáticos en ASP.NET Core.
Al final acabamos utilizando la extensión Bundler & Minifier de Mads Kristensen, sobre la que quizás escriba una entrada específica en el blog. En resumen, especificamos los archivos de entrada y salida en un archivo bundleconfig.json
:
[
{
"outputFileName": "wwwroot/bundles/style.min.css",
"inputFiles": [
"wwwroot/css/style1.css",
"wwwroot/css/style2.css"
]
}
]
Este archivo se procesa con Gulp y se hace el bundling y la minificación con los paquetes gulp-cssmin
, gulp-concat
, y otros paquetes npm similares (tenemos un curso fenomenal de herramientas Front-End aquí). Esto hace posible ejecutarlo tanto localmente (conectando las tareas de Gulp al build de Visual Studio si lo deseas), como en nuestro servidor de Azure DevOps.
Mira mamá, no más peticiones potencialmente peligrosas
Si has estado ejecutando ASP.NET MVC y registrando peticiones fallidas, probablemente ya has visto este error muchas veces:
Se ha detectado un valor potencialmente peligroso en Request.Form
ASP.NET MVC no permitía etiquetas <script>
y similares como parte de las peticiones POST a los controladores de MVC. ASP.NET Core cambió eso y acepta entradas como esa. Si para ti es importante securizar la aplicación contra contenidos como ese, necesitarás añadir un filtro o algo similar que los compruebe. En nuestro caso, "escapeamos" todo usando Knockout.js, por lo que hacer peticiones en las que haya fallos de marcado no tiene importancia. Pero es algo que sin duda hay que tener en cuenta.
Funciones de Azure
Migrar a la versión más reciente de las Funciones de Azure (actualmente la v3) ha sido un proceso largo. Muchas de las características de elmah.io se basan en tareas programadas y servicios que están en ejecución mucho tiempo, como por ejemplo consumir mensajes desde el Azure Service Bus, enviar un correo electrónico de resumen diario, etc. Hace unos años, todos estos "trabajos" se ejecutaban como tareas programadas de Windows y servicios de Windows en una máquina virtual en Azure. Migramos todos estos servicios menos uno a Azure Functions v1 ejecutándose en .NET Framework.
Cuando salió Azure Functions v2, comenzamos a migrar una aplicación de funciones a v2 ejecutándose en .NET Core con resultados pobres. Existían todo tipo de problemas y el conjunto disponible de clases de la biblioteca base no era lo suficientemente bueno. Cuando salió .NET Core 2.2 finalmente dimos el salto y portamos todas las funciones.
Como parte de la reciente tarea de migración, migramos todas las aplicaciones de funciones a Azure Functions v3 ejecutándose en .NET Core 3.1. El código se ha estado ejecutando extremadamente estable desde que lo hicimos, y yo recomendaría esta configuración para su uso en producción.
Cómo hacer la actualización
La actualización fue mucho más rápida que la de ASP.NET Core. Las versiones v1, v2 y v3 siguen más o menos la misma estructura de archivos y la mayor parte del conjunto de características. Para actualizar, simplemente actualicé el marco de trabajo de cada proyecto así como todos los paquetes NuGet.
Usar Microsoft.Azure.Functions.Extensions
Si tú, como nosotros, comenzaste a crear Funciones de Azure con la versión 1 del runtime y .NET "clásico", probablemente te habrás preguntado cómo hacer la inyección de dependencias o la inicialización de las funciones. Al actualizar a la v2 y .NET Core 2.2 comenzamos a usar el paquete NuGet de Microsoft.Azure.Functions.Extensions
. Una vez instalado, puedes especificar un archivo Startup.cs
en la app de tu Function, tal como lo habrías hecho en ASP.NET Core. Allí, puedes abrir las conexiones de la base de datos, configurar el registro, etc:
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
[assembly: FunctionsStartup(typeof(My.Function.Startup))]
namespace My.Function
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddHttpClient(...);
var config = new ConfigurationBuilder()
.AddJsonFile("local.settings.json")
.Build();
builder.Services.AddLogging(logging =>
{
logging.AddSerilog(dispose:true);
});
builder.Services.AddSingleton<IFunctionFilter, PerformanceFilter>();
}
}
}
El código anterior es tan solo un conjunto aleatorio de líneas de configuración que he elegido de una de nuestras funciones. Aunque debería resultar familiar para los desarrolladores de ASP.NET Core.
No uses los bindings de salida
Una de las cosas buenas de las Azure Functions es un catálogo cada vez mayor de bindings de entrada y salida. ¿Quieres ejecutar la función cuando se escribe un nuevo blob en el almacenamiento de blobs? Añade un binding de entrada a blobs. ¿Escribir un mensaje en una cola una vez que la función se ha completado? Añade un binding de salida al bus de servicio... Teníamos un viejo código portado de Servicios de Windows, que hacía toda esta comunicación manualmente y me interesaba empezar a utilizar los bindings de salida de las Azure Functions cuando lo portásemos a v3.
Al final acabé echando para atrás la mayoría de estos cambios y evitando los bindings de salida por completo. Aunque están muy bien pensados, pierdes el control de lo que sucede en esos bindings. Además, cada binding se implementa de forma diferente. Algunos con reintentos, otros sin ello. Esta respuesta de Stephen Cleary lo explica bastante bien. En la iteración de código más reciente, he creado todos los clientes de la base de datos, clientes de temas, etc. en el archivo Startup.cs
y los inyecto en el constructor de cada función. Así tengo el control total de cuándo hacer peticiones, cuántos reintentos quiero ejecutar, etc. Otra ventaja es que el código de la función ahora se parece mucho al código de los sitios web de ASP.NET Core, que inicializa e inyecta las dependencias de la misma manera.
Herramientas
Antes de terminar, quiero decir unas palabras sobre las herramientas de migración automática. Cuando comenzamos el proyecto de migración existían algunas opciones. Y desde entonces han aparecido aún más. No he probado ninguna de ellas, pero ahora hay tantas opciones disponibles que, si empezase hoy con la migración probablemente lo haría. Échales un vistazo:
Conclusión
Migrar ha sido, en conjunto, una gran decisión para nosotros. Ya le estamos viendo cantidad de ventajas. Un framework más simple. Tiempos de build más rápidos. La compilación de Razor es mucho más rápida. Más fácil trabajar en código para los que prefieren hacerlo así. Mejor rendimiento en Azure y menos consumo de recursos (principalmente memoria). La posibilidad de mover el hosting a Linux. Y mucho más. Dicho esto, la migración llevó mucho tiempo.
Las secciones de antes las he ido escrito de lo que recordaba mientras escribía el post. Puede que quiera incluir más secciones cuando recuerde algo o encuentre nuevos temas. Si tienes algún tema específico sobre el que quieras aprender más o preguntas concretas sobre la migración, no dudes en contactar con nosotros en elmah.io.