← maurobernal.com.ar

Etiqueta: tooling

  • Guía práctica: cómo migré mis proyectos de .NET 8 a .NET 10 sin romper producción

    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

    PasoAcciónImpacto
    1Cambiar TFM a net10.0 en todos los .csprojObligatorio
    2dotnet restore && dotnet buildObligatorio
    3Ejecutar suite de tests completaObligatorio
    4Actualizar paquetes Microsoft.* a versión 10.xRecomendado
    5Adoptar field en propiedades con backing fieldGradual
    6Primary Constructors en servicios con DIGradual
    7Reemplazar object _lock por Lock _lockGradual
    8Migrar OpenAPI de Swashbuckle a nativoOpcional
    9Adoptar params ReadOnlySpan<T> en hot pathsOpcional
    10Evaluar Native AOT para Workers/APIs simplesOpcional 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

  • dotnet run script.cs y las nuevas herramientas de .NET 10: OpenAPI, HttpClient y más

    Uno de los cambios de .NET 10 que más me llamó la atención no es del compilador ni del runtime: es de las herramientas. dotnet run script.cs permite ejecutar un archivo C# sin crear un proyecto. Y el soporte OpenAPI nativo en Minimal APIs me hizo desinstalar Swashbuckle de varias soluciones. Repaso todo el tooling nuevo.

    dotnet run script.cs: C# sin proyecto

    Antes de .NET 10, ejecutar un archivo C# requería crear un proyecto, un .csproj, restaurar paquetes… Para un script rápido o una prueba de concepto, era excesivo. Ahora podés ejecutar un archivo directamente.

    // script.cs — archivo suelto, sin .csproj
    using System.Net.Http.Json;
    
    var http = new HttpClient();
    var usuarios = await http.GetFromJsonAsync<List<Usuario>>(
        "https://jsonplaceholder.typicode.com/users"
    );
    
    foreach (var u in usuarios ?? [])
        Console.WriteLine($"{u.Id}: {u.Name} ({u.Email})");
    
    record Usuario(int Id, string Name, string Email);
    # Ejecutar directamente
    dotnet run script.cs
    
    # Con referencias a paquetes NuGet (directivas en el archivo)
    # #:package Newtonsoft.Json@13.0.3
    
    # Con argumentos
    dotnet run script.cs -- arg1 arg2
    
    # Ver el IL generado sin ejecutar
    dotnet run --no-build script.cs

    Uso esto constantemente para probar APIs externas, procesar archivos CSV, o generar datos de prueba. Antes abría LINQPad; ahora uso la terminal directamente.

    OpenAPI built-in en Minimal APIs (.NET 9+)

    Desde .NET 9, la documentación OpenAPI está integrada en el framework. No hace falta Swashbuckle ni NSwag para tener un endpoint /openapi/v1.json funcional.

    // Program.cs — OpenAPI nativo en .NET 9/10
    var builder = WebApplication.CreateBuilder(args);
    
    // Solo agregar esto:
    builder.Services.AddOpenApi();
    
    var app = builder.Build();
    
    // Endpoint para servir el documento OpenAPI
    app.MapOpenApi();  // /openapi/v1.json por defecto
    
    // Opcional: UI de Swagger (sigue siendo un paquete externo)
    // app.UseSwaggerUI(options => options.SwaggerEndpoint("/openapi/v1.json", "Mi API"));
    
    // Minimal API con metadata para OpenAPI
    app.MapGet("/api/pedidos/{id}", async (int id, IRepositorio repo) =>
    {
        var pedido = await repo.ObtenerAsync(id);
        return pedido is null ? Results.NotFound() : Results.Ok(pedido);
    })
    .WithName("ObtenerPedido")
    .WithSummary("Obtiene un pedido por ID")
    .WithDescription("Retorna el pedido completo con sus líneas de detalle")
    .WithTags("Pedidos")
    .Produces<Pedido>(200)
    .Produces(404);
    
    app.Run();

    HttpClient: mejoras en .NET 10

    // .NET 10: HttpClient con Resilience nativo (Microsoft.Extensions.Http.Resilience)
    builder.Services.AddHttpClient<IMiApiClient, MiApiClient>(client =>
    {
        client.BaseAddress = new Uri("https://api.externa.com");
        client.Timeout = TimeSpan.FromSeconds(30);
    })
    .AddStandardResilienceHandler();  // retry + circuit breaker + timeout automáticos
    
    // Configuración personalizada del pipeline de resiliencia
    builder.Services.AddHttpClient<IMiApiClient, MiApiClient>()
        .AddResilienceHandler("mi-pipeline", builder =>
        {
            builder.AddRetry(new HttpRetryStrategyOptions
            {
                MaxRetryAttempts = 3,
                Delay = TimeSpan.FromMilliseconds(500),
                BackoffType = DelayBackoffType.Exponential
            });
            
            builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
            {
                FailureRatio = 0.5,
                SamplingDuration = TimeSpan.FromSeconds(10),
                MinimumThroughput = 5
            });
            
            builder.AddTimeout(TimeSpan.FromSeconds(10));
        });

    Mejoras en Minimal APIs

    // .NET 9/10: TypedResults para respuestas con tipos bien definidos
    app.MapPost("/api/usuarios", async (CrearUsuarioRequest req, IServicio svc) =>
    {
        if (string.IsNullOrEmpty(req.Email))
            return TypedResults.ValidationProblem(new Dictionary<string, string[]>
            {
                ["email"] = ["El email es requerido"]
            });
    
        var usuario = await svc.CrearAsync(req);
        return TypedResults.Created($"/api/usuarios/{usuario.Id}", usuario);
    });
    
    // Route groups para organizar endpoints
    var pedidosGroup = app.MapGroup("/api/pedidos")
        .WithTags("Pedidos")
        .RequireAuthorization();
    
    pedidosGroup.MapGet("/", ObtenerTodos);
    pedidosGroup.MapGet("/{id}", ObtenerPorId);
    pedidosGroup.MapPost("/", Crear);
    pedidosGroup.MapPut("/{id}", Actualizar);
    pedidosGroup.MapDelete("/{id}", Eliminar);

    Blazor en .NET 10: render modes y streaming

    // .NET 10: componentes Blazor con render modes explícitos
    @page "/dashboard"
    @rendermode InteractiveServer
    
    <h1>Dashboard</h1>
    
    @if (datos is null)
    {
        <p>Cargando...</p>
    }
    else
    {
        @foreach (var item in datos)
        {
            <div>@item.Nombre: @item.Valor</div>
        }
    }
    
    @code {
        private List<Dato>? datos;
    
        protected override async Task OnInitializedAsync()
        {
            // Streaming: el componente renderiza mientras carga
            await foreach (var dato in servicio.GetStreamAsync())
            {
                datos ??= [];
                datos.Add(dato);
                StateHasChanged();  // actualiza el DOM incremental
            }
        }
    }

    ← Rendimiento extremo en .NET 10: Stack allocation, Native AOT y el GC que trabaja menos | Serie .NET 8 → .NET 10 | Próximo: Guía práctica: cómo migré mis proyectos de .NET 8 a .NET 10 sin romper producción →

Tags

tsql (27)mssql (26)sql (20)devops (20)dotnet (18)docker (15)performance (14)contenedores (11)dotnet10 (10)linux (9)csharp (8)microservicios (7)angular (7)angular21 (7)sql server (6)issabel (6)docker-compose (6)typescript (6)mysql (5).NET (5)