Guía para modernizar aplicaciones empresariales en .NET

🏠 Entorno de trabajo 📅 04 May 2026 ⏱️ 5 min 💬 0

Mudarse de un .NET Framework "legacy" (o incluso un 4.8) a un .NET moderno suena, en teoría, como una decisión obvia. Sin embargo, en la práctica, cuando tienes un producto empresarial grande, con aplicaciones en IIS, App Services de Windows, una API robusta, un front en React, una app interna tipo CRM y un Windows Service haciendo trabajo en segundo plano, la cosa deja de parecer una simple “actualización”. El trabajo se complica muchísimo más de lo esperado.

Este es un tema que menciono mucho en mis vídeos, ya que sois muchos los que estáis atascados sin poder dar el salto. Lo que os debería preocupar a la mayoría no es el “no poder utilizar contenedores o cloud functions”; el verdadero problema al que os enfrentáis es saber que cada vez cuesta más hacer cualquier cambio en el sistema.

Como he vivido esto en mis propias carnes (y varias veces), vamos a hablar del tema y a ponerle solución.

 

 

1 - Aquí no ganas con una gran migración heroica

 

La peor idea para un sistema de este calibre suele ser la más tentadora: abrir una nueva branch que pretenda solucionarlo todo de golpe, intentar migrar el 100% a .NET 8 o .NET 10, arreglar miles de errores de compilación y rezar para que al final todo siga funcionando. Spoiler: eso casi nunca sale bien.

El problema no es solo técnico, es de visibilidad. Este cambio va a tardar meses, y durante ese tiempo, ni el cliente ni el equipo de negocio ven un progreso utilizable; solo ven coste, riesgo y trabajo acumulado.

Esto significa que, en vez de hacer un Big Bang, lo que tenemos que buscar es ir haciendo pequeñas migraciones. Pasos cortos que mejoren el sistema poco a poco. Y aunque el producto final siga apoyándose en .NET Framework por un tiempo, cada paso que demos mejorará ligeramente el ecosistema. Esto aporta esos quick wins que son vitales para mantener la moral y, por qué no decirlo, justificar el trabajo.

 

2 - Duplicar no siempre está mal: Strangler Fig al rescate

 

Cuando tenemos un sistema enorme, con miles o millones de líneas de código, el modelo que mejor funciona no es “reescribir”, sino duplicar el sistema antiguo poco a poco. Es decir: crear una aplicación nueva en .NET moderno que conviva con la existente. Esta nueva app irá recibiendo tráfico y absorbiendo rutas, módulos o capacidades gradualmente, mientras lo que aún no se ha migrado sigue sirviéndose desde .NET Framework.

La idea (conocida como el patrón Strangler Fig) es simple: no sustituyes el sistema antiguo de golpe. Pones una capa nueva delante (normalmente en .NET moderno) y vas moviendo piezas una a una.

Durante un tiempo, tu arquitectura se verá algo así:

Usuario / Frontend
        |
        v
Fachada moderna (.NET 8/10)
        |
        +--> Nueva funcionalidad en .NET moderno
        |
        +--> Sistema legacy en .NET Framework 4.8

 

Desde fuera, el producto sigue siendo uno solo. Por dentro, estás cambiando el motor en pleno vuelo. De esta forma, no necesitas decidir en una sola tarde cómo se moderniza todo el dominio, la API pública o los procesos asíncronos. Solo necesitas decidir qué parte puede vivir primero fuera de la aplicación legacy sin hacer un destrozo.

Lo interesante aquí es que este modelo te obliga a identificar los límites reales de tu sistema, y eso ya es una gran victoria. Muchas veces el problema no es que el código sea “viejo”, sino que la lógica de negocio, el acceso a datos y la presentación viven demasiado entremezclados.

Ojo, esto tiene una contrapartida: tendrás que mantener tanto el código legacy como el nuevo mientras dure la transición. Por otro lado, reduce muchísimo el riesgo, ya que puedes controlar con una simple feature flag si el tráfico va por el proceso nuevo o por el antiguo.

 

3 - Empieza por lo que se mueve fácil, no por lo que más ganas tengas de migrar

 

Sé que la frustración inmediata suele ser “no puedo usar contenedores”, “esto tarda mucho en compilar” o “no puedo meter esto en una Lambda”. Estas quejas son reales, todos las hemos tenido, pero para llegar a ese punto de modernidad absoluta hay que tener la casa ordenada. Podemos empezar migrando lo más obvio: la típica tarea en segundo plano o algún proceso que se ejecute de forma asíncrona cada cierto tiempo.

Estos procesos tienen una "arquitectura" sencilla: reciben el trabajo, lo procesan, completan o fallan (quizás con algo de reintentos), pero no suelen ir mucho más allá.

Por ejemplo, este podría ser un proceso en un Windows Service clásico:

public partial class InvoiceWorkerService : ServiceBase
{
    private Timer _timer;

    protected override void OnStart(string[] args)
    {
        _timer = new Timer(ProcessPendingInvoices, null, TimeSpan.Zero, TimeSpan.FromMinutes(5));
    }

    protected override void OnStop()
    {
        _timer?.Dispose();
    }

    private void ProcessPendingInvoices(object state)
    {
        var processor = new InvoiceProcessor();
        processor.Process();
    }
}

 

Y de ahí, podemos llevárnoslo fácilmente a un BackgroundService en .NET moderno:

public sealed class InvoiceBackgroundWorker : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<InvoiceBackgroundWorker> _logger;

    public InvoiceBackgroundWorker(
        IServiceScopeFactory scopeFactory,
        ILogger<InvoiceBackgroundWorker> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));

        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                await using var scope = _scopeFactory.CreateAsyncScope();
                var processor = scope.ServiceProvider.GetRequiredService<InvoiceProcessor>();

                await processor.ProcessAsync(stoppingToken);
            }
            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
            {
                throw;
            }
            catch (Exception exception)
            {
                _logger.LogError(exception, "Error processing pending invoices.");
            }
        }
    }
}

 

El funcionamiento de negocio es el mismo, pero ahora está corriendo en un entorno actualizado y desacoplado. Realizar este tipo de pequeñas victorias es crucial en una migración de gran escala. Afirmar en una reunión que "todo irá bien cuando terminemos" no es suficiente; necesitamos demostrar empíricamente, paso a paso, que lo que decimos es cierto.

 

4 - Planifica para cumplir objetivos

 

He trabajado tanto en migraciones que se han eternizado, como en otras que tomaron el tiempo correcto. Las que funcionaron bien tenían una cosa en común: una buena planificación. Como ya he dicho, si intentas modernizar todo de golpe, estás muerto. Hay que ir poco a poco, identificando objetivos claros. Algunos pasos lógicos suelen ser:

  • Comprobar qué partes pueden ser empaquetadas en el nuevo formato SDK de proyectos (.csproj).
  • Revisar los pasos de la build para asegurarnos de que no hay "hacks" extraños (y si los hay, entender el motivo y solucionarlos).
  • Actualizar las librerías de terceros que no funcionan fuera de Windows. Esta es la parte más obvia si tu objetivo es acabar corriendo en contenedores Linux (lo cual abarata muchísimo los costes).
  • Eliminar o sustituir dependencias que ya no reciben soporte. Es crítico tener siempre versiones estables y seguras; si una librería está deprecated, este es el momento perfecto para jubilarla.

Si te fijas, cumplir solo uno de estos puntos te puede llevar semanas o meses, dependiendo de lo acoplado que esté el sistema. Así que en realidad no tienes una migración en mente, sino varias sub-migraciones. No hace falta montar una arquitectura hexagonal de libro en toda la solución desde el día 1; basta con dejar de esparcir código de dependencias obsoletas por toda la app, o al menos ocultarlas tras una interfaz.

 

5 - Migrar no es el problema. Eternizarlo sí.

 

A estas alturas, la duda de si merece la pena salir de .NET 4.8 casi sobra. Cuando tu stack tecnológico empieza a limitar cómo despliegas, dónde puedes hostear o qué opciones operativas tienes, la respuesta está clara.

La verdadera pregunta es: ¿Cómo haces esto sin que se convierta en un proyecto eterno que compita (y pierda) sistemáticamente contra las nuevas features del producto?

La clave para venderle esto a tus superiores es: reducir el impacto, conseguir pequeñas victorias, no mezclar demasiadas incógnitas a la vez y asegurar que en cada iteración salimos un poco menos atados que antes. En mi experiencia, en cuanto se migran un par de módulos y los beneficios (rendimiento, facilidad de despliegue, DX) son palpables, suele haber luz verde para continuar. Eso sí, asume que la migración completa llevará años y requerirá el esfuerzo de varios equipos.

Por cierto, el ecosistema .NET ofrece herramientas como el .NET Upgrade Assistant que pueden ayudar a automatizar parte del trabajo pesado inicial: detectar incompatibilidades, convertir proyectos y darte una base sobre la que iterar. No te van a resolver la arquitectura mágicamente, pero ahorran dolores de cabeza.

Y recuerda: cada año que pasa, cada funcionalidad extra que añades al legacy, hace más difícil salir de ahí. Empezar hoy siempre será mejor que hacerlo mañana. Trátalo como una iniciativa de producto a largo plazo, y llegarás a buen puerto.


💬 Comentarios