Migrar un proyecto de .NET 8 a .NET 10 es el tipo de tarea que parece riesgosa pero en la práctica es incremental y reversible. Lo hice en varios proyectos de producción sin downtime. El proceso siempre es el mismo: actualizar el TFM, correr los tests, adoptar las nuevas features de a poco.
Paso 1: Actualizar el Target Framework
# Verificar SDK instalado
dotnet --list-sdks
# Instalar .NET 10 SDK si no está
# Linux/WSL:
wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh
bash dotnet-install.sh --channel 10.0
# En cada .csproj de la solución:
# <TargetFramework>net10.0</TargetFramework>
# Para actualizar todos los .csproj de una solución en Linux:
find . -name "*.csproj" -exec sed -i 's/net8.0/net10.0/g; s/net9.0/net10.0/g' {} \;
# Restaurar y compilar
dotnet restore
dotnet build
Paso 2: Actualizar paquetes NuGet
# Ver paquetes desactualizados
dotnet list package --outdated
# Actualizar todos los paquetes de Microsoft.* a versiones compatibles con .NET 10
dotnet add package Microsoft.AspNetCore.OpenApi
dotnet add package Microsoft.Extensions.Http.Resilience
# Paquetes que pueden desinstalarse en .NET 10:
# - Swashbuckle.AspNetCore (reemplazado por OpenAPI nativo)
# - Microsoft.AspNetCore.Mvc.NewtonsoftJson (System.Text.Json mejorado)
# Remover Swashbuckle si usabas OpenAPI:
dotnet remove package Swashbuckle.AspNetCore
Paso 3: Adoptar las nuevas features de C# 14
No hay que cambiar nada para que el proyecto compile. Las nuevas features son opt-in. La estrategia es adoptarlas gradualmente donde aporten más valor.
// Prioridad 1: field — máximo impacto con mínimo riesgo
// Buscar propiedades con backing fields privados y migrarlas
// Antes:
private string _nombre = string.Empty;
public string Nombre
{
get => _nombre;
set => _nombre = value?.Trim() ?? throw new ArgumentNullException();
}
// Después:
public string Nombre
{
get => field;
set => field = value?.Trim() ?? throw new ArgumentNullException();
}
// Prioridad 2: Primary Constructors en servicios con DI
// Especialmente útil en proyectos con muchos servicios inyectados
// Prioridad 3: params ReadOnlySpan en métodos variádicos de uso frecuente
// Buscar: params int[], params string[], params object[]
// Reemplazar por: params ReadOnlySpan<T> donde sea posible
// Prioridad 4: Lock para thread safety
// Buscar: private readonly object _lockObj = new object();
// Reemplazar por: private readonly Lock _lockObj = new();
Paso 4: Migrar a OpenAPI nativo (si usabas Swashbuckle)
// Program.cs — reemplazar Swashbuckle por OpenAPI nativo
// ❌ Antes con Swashbuckle:
// builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", ...); });
// app.UseSwagger();
// app.UseSwaggerUI();
// ✅ .NET 9/10 nativo:
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, context, ct) =>
{
document.Info = new()
{
Title = "Mi API",
Version = "v1",
Description = "API de gestión interna"
};
return Task.CompletedTask;
});
});
// En el pipeline:
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
// Opcional: seguir usando Swagger UI solo para dev
app.UseSwaggerUI(options =>
options.SwaggerEndpoint("/openapi/v1.json", "Mi API v1"));
}
Checklist de migración completo
| Paso | Acción | Impacto |
|---|---|---|
| 1 | Cambiar TFM a net10.0 en todos los .csproj | Obligatorio |
| 2 | dotnet restore && dotnet build | Obligatorio |
| 3 | Ejecutar suite de tests completa | Obligatorio |
| 4 | Actualizar paquetes Microsoft.* a versión 10.x | Recomendado |
| 5 | Adoptar field en propiedades con backing field | Gradual |
| 6 | Primary Constructors en servicios con DI | Gradual |
| 7 | Reemplazar object _lock por Lock _lock | Gradual |
| 8 | Migrar OpenAPI de Swashbuckle a nativo | Opcional |
| 9 | Adoptar params ReadOnlySpan<T> en hot paths | Opcional |
| 10 | Evaluar Native AOT para Workers/APIs simples | Opcional avanzado |
Errores comunes y cómo resolverlos
// ERROR 1: Paquete no compatible con net10.0
// "Package X is not compatible with net10.0"
// Solución: actualizar a la versión más reciente del paquete
// Si no hay versión compatible, usar <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
// ERROR 2: Reflection en Native AOT
// "System.InvalidOperationException: Type ... is not reflection-enabled"
// Solución: agregar source generator o usar [DynamicDependency]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(MiClase))]
static void MetodoConReflection() { ... }
// ERROR 3: field en C# 14 — feature no disponible
// "error CS8652: The feature 'field keyword' is currently in Preview"
// Solución: habilitar preview features en .csproj
// <LangVersion>preview</LangVersion>
// (o esperar a que salga de preview en la versión final de .NET 10)
// ERROR 4: Cambios breaking en serialización JSON
// System.Text.Json en .NET 10 es más estricto con algunos tipos
// Solución: revisar JsonSerializerOptions y ajustar si es necesario
Mi experiencia: cuánto tardó cada proyecto
- API Minimal con 15 endpoints: 30 minutos (cambiar TFM, actualizar paquetes, correr tests)
- Worker Service con threading complejo: 2 horas (migración de locks + tests de concurrencia)
- Aplicación ASP.NET MVC grande (80 controllers): medio día (TFM + paquetes + resolver warnings)
- Adopción gradual de C# 14 features: 2-3 semanas en paralelo con desarrollo normal
La migración del TFM en sí es rápida. Lo que toma tiempo es la adopción de las nuevas features, y eso es completamente optativo. Podés migrar a .NET 10 hoy y adoptar field o Primary Constructors a tu ritmo, archivo por archivo, cuando tocás esa parte del código.
← dotnet run script.cs y las nuevas herramientas de .NET 10: OpenAPI, HttpClient y más | Fin de la Serie .NET 8 → .NET 10
Deja una respuesta