C# 13 y .NET 9 en 2026: las novedades que ya puedes usar en producción

Desde .NET 5, Microsoft publica una versión mayor cada noviembre. Lo que varía es cuánto tiempo recibe soporte cada una: las versiones pares son LTS (Long Term Support, tres años) y las impares son STS (Standard Term Support, dieciocho meses).

.NET 9 salió en noviembre de 2024 y es STS, así que su soporte termina en mayo de 2026. Si en este momento vas a arrancar un proyecto nuevo que necesite estabilidad a largo plazo, .NET 10 (noviembre de 2025, LTS) es la opción más sensata. Pero las novedades de C# 13 y .NET 9 ya están disponibles también en .NET 10, así que todo lo que leas aquí te aplica directamente.

Dicho esto, hay muchos equipos con proyectos en .NET 8 LTS que están valorando el salto a .NET 9 o esperando a .NET 10. Conocer qué trae .NET 9 ayuda a decidir si merece la pena migrar ahora o esperar.

params con cualquier tipo de colección

Hasta C# 12, la palabra clave params solo funcionaba con arrays. Esto limitaba bastante su utilidad porque cualquier método que aceptara una lista o un span como parámetro no podía beneficiarse de la sintaxis de llamada cómoda.

C# 13 elimina esa restricción. Ahora puedes declarar:

void Log(params IEnumerable<string> mensajes) { ... }
void Procesar(params ReadOnlySpan<int> numeros) { ... }
void Añadir(params List<string> items) { ... }

El código de llamada no cambia nada:

Log("inicio", "proceso", "fin");
Procesar(1, 2, 3, 4);

La ventaja más interesante es params ReadOnlySpan<T>: cuando llamas al método con literales, el compilador puede colocar los valores en la pila y evitar una asignación en el heap. Para métodos que se llaman muchas veces en bucles de alto rendimiento, es una mejora sin coste de legibilidad.

El nuevo System.Threading.Lock

El patrón clásico de sincronización en C# ha sido siempre este:

private readonly object _lock = new object();

public void MetodoSincronizado()
{
    lock (_lock)
    {
        // sección crítica
    }
}

Funciona, pero object no tiene ninguna semántica de bloqueo: es un tipo genérico que se usa aquí solo por convención. Si alguien pasa ese object a otro método y lo usa mal, el compilador no puede advertirte.

.NET 9 añade System.Threading.Lock, un tipo específico para esto:

private readonly Lock _lock = new Lock();

public void MetodoSincronizado()
{
    lock (_lock)
    {
        // sección crítica
    }
}

El compilador reconoce el tipo Lock y genera un código más eficiente que con object. Además, las herramientas de análisis pueden detectar usos incorrectos con más precisión. La migración es trivial: cambiar el tipo de la variable es todo lo que necesitas.

LINQ: CountBy, AggregateBy e Index

.NET 9 añade tres métodos a LINQ que cubren casos muy comunes sin tener que pasar por GroupBy.

CountBy

CountBy cuenta cuántos elementos hay por clave:

var productos = new[]
{
    new { Nombre = "A", Categoria = "Electrónica" },
    new { Nombre = "B", Categoria = "Ropa" },
    new { Nombre = "C", Categoria = "Electrónica" },
};

foreach (var (categoria, total) in productos.CountBy(p => p.Categoria))
{
    Console.WriteLine($"{categoria}: {total}");
}
// Electrónica: 2
// Ropa: 1

Antes tenías que hacer GroupBy(...).Select(g => (g.Key, g.Count())). Con CountBy es más directo y el runtime puede optimizarlo mejor.

AggregateBy

Es el equivalente para agregaciones arbitrarias. En vez de agrupar y luego agregar, combinas los dos pasos:

var ventas = new[]
{
    new { Vendedor = "Ana", Importe = 100m },
    new { Vendedor = "Luis", Importe = 200m },
    new { Vendedor = "Ana", Importe = 150m },
};

var totalesPorVendedor = ventas.AggregateBy(
    v => v.Vendedor,
    seed: 0m,
    (acumulado, venta) => acumulado + venta.Importe
);

foreach (var (vendedor, total) in totalesPorVendedor)
    Console.WriteLine($"{vendedor}: {total}€");
// Ana: 250€
// Luis: 200€

Index

Index() devuelve cada elemento junto con su posición, algo que en Python se hace con enumerate y que en C# antes requería un contador manual o un Select con el índice:

var frutas = new[] { "manzana", "pera", "naranja" };

foreach (var (i, fruta) in frutas.Index())
{
    Console.WriteLine($"{i}: {fruta}");
}
// 0: manzana
// 1: pera
// 2: naranja

System.Text.Json en .NET 9

La serialización JSON ha recibido varias mejoras que reducen la configuración manual en casos habituales.

JsonSerializerOptions.Web es una instancia preconfigurada con los ajustes más comunes para APIs web: nombres de propiedades en camelCase, lectura flexible de números, etc. En vez de crear y configurar tus propias opciones, puedes usar:

var json = JsonSerializer.Serialize(objeto, JsonSerializerOptions.Web);
var resultado = JsonSerializer.Deserialize<MiClase>(json, JsonSerializerOptions.Web);

Otra mejora es el soporte mejorado de nullable reference types. Si tienes una clase con una propiedad string?, el serializador ahora infiere correctamente que puede ser null y no lanza excepciones inesperadas al deserializar JSON donde esa propiedad no aparece.

También está el atributo [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)], que le dice al serializador que rellene las propiedades de un objeto ya existente en vez de crear uno nuevo. Esto es útil cuando tienes objetos con estado previo que quieres actualizar parcialmente desde JSON.

SearchValues<string>

.NET 8 trajo SearchValues<char> para buscar caracteres concretos en un span de texto de forma muy eficiente. .NET 9 extiende eso a strings completos con SearchValues<string>.

El caso de uso típico es validar texto contra un conjunto de palabras prohibidas o buscar múltiples keywords en un log grande:

private static readonly SearchValues<string> PalabrasBloqueadas =
    SearchValues.Create(["spam", "phishing", "fraude"], StringComparison.OrdinalIgnoreCase);

bool ContieneContenidoProhibido(ReadOnlySpan<char> texto)
{
    return texto.ContainsAny(PalabrasBloqueadas);
}

Por dentro usa el algoritmo Aho-Corasick, que busca todas las palabras en una sola pasada sobre el texto. Para textos largos o colecciones grandes de palabras clave, la diferencia de rendimiento frente a hacer varias llamadas a Contains es notable.

Rendimiento: mejoras en el JIT de .NET 9

Cada versión de .NET trae mejoras en el compilador JIT, y .NET 9 no es una excepción. Hay tres áreas donde el salto es más visible.

La primera son las optimizaciones de bucles. El JIT detecta y vectoriza más patrones de iteración automáticamente, sin que tengas que escribir código SIMD a mano. Si tienes bucles sobre arrays o spans de números, pueden acelerarse sin tocar nada.

La segunda es el soporte de AVX-512 en procesadores x64 compatibles. AVX-512 permite operar sobre 512 bits de datos en paralelo, el doble que AVX2. El JIT aprovecha esto para operaciones numéricas intensivas cuando el hardware lo permite.

La tercera es un pequeño helper que reduce el coste de verificar si un objeto ha sido descartado:

// Antes
if (_disposed) throw new ObjectDisposedException(nameof(MiClase));

// .NET 9
ObjectDisposedException.ThrowIf(_disposed, this);

No es una mejora de rendimiento masiva, pero sí evita crear el objeto de excepción hasta que sea necesario y hace el código más limpio.

En benchmarks generales, .NET 9 sale entre un 5 % y un 15 % más rápido que .NET 8 en operaciones con mucha CPU. Los resultados varían según el tipo de carga, pero la tendencia es consistente.

¿Migrar a .NET 9 ahora o esperar a .NET 10?

Si tu proyecto está en .NET 8 LTS y no tienes urgencia, esperar a .NET 10 (noviembre de 2025, LTS) tiene sentido: te da tres años de soporte desde el principio sin tener que hacer dos migraciones seguidas.

Si quieres probar las novedades ya, o tienes proyectos internos sin restricciones de soporte a largo plazo, .NET 9 es perfectamente válido. La migración desde .NET 8 suele ser sencilla: cambiar el TargetFramework en el proyecto y actualizar los paquetes NuGet.

Para fijar la versión del SDK en el repositorio y evitar que el entorno de cada desarrollador use una versión distinta, usa un archivo global.json:

{
  "sdk": {
    "version": "9.0.100",
    "rollForward": "latestFeature"
  }
}

Si vienes de .NET 6 o .NET 7, la herramienta oficial dotnet-upgrade-assistant automatiza buena parte del proceso:

dotnet tool install -g upgrade-assistant
upgrade-assistant upgrade MiProyecto.csproj

Para saber más sobre la historia del lenguaje y cómo ha llegado hasta aquí, puedes leer C#: el lenguaje que evolucionó a la par que .NET. Y si trabajas con APIs web, en ASP.NET Core y la evolución del stack web de Microsoft tienes el contexto del lado servidor.

Imagen: Pexels / Marta Branco

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP