pingedbrain
EN

3 min de lectura Read in English

.NET 10 para microservicios: Por qué le gana a TypeScript y Go en producción

Después de migrar la API de RBAC de korz.sh de TypeScript a .NET 10, esto es lo que aprendí sobre por qué el framework tiene sentido para trabajo serio de backend.

Hace un año si me preguntabas qué usar para microservicios te decía TypeScript o Go sin pensarlo. Están en todos lados, funcionan, y las conozco bien.

Mi historia real empieza con Go. Lo usé para korz.sh al principio y andaba bien para cosas pequeñas. Pero cuando el servicio empezó a escalar — más endpoints, más reglas de negocio, más edge cases — me encontré creando mis propias libs para cosas que cualquier framework serio ya tiene resuelto. A las dos semanas estaba re-inventando la rueda con algo que en .NET viene de fábrica. Como soy solo una persona, no tenía tiempo de mantener libs propias mientras también construía el producto.

Así que migré a TypeScript. Más rápido de iterar, más libraries disponibles, el equipo de frontend ya lo conocía. TypeScript está bien para prototipar, pero cuando empezás a necesitar cosas serias — validación que no sea un quilombo, auth que no sea artesanal, observabilidad que funcione — terminás pegando muchos packages y configurando muchas cosas a mano.

Cuando la performance importó de verdad, migré a .NET 10.


La validación que nadie te explica bien

Zod está bien. Pero el problema no es Zod — el problema es que la validación de endpoints en TypeScript crece al pedo. Necesitás el schema para typesafety, después necesitás un validator runtime que matchée con el schema, después el schema se desincroniza con el modelo de la base de datos y nadie se entera hasta que algo rompe en producción.

En Go ni hay stdlib para esto. Usás go-playground/validator o alguna librería similar que anda, pero la DX es verbosa.

FluentValidation en .NET cambia eso de una forma que no sabía que existía hasta que la usé:

public class CreateApiKeyValidator : AbstractValidator<CreateApiKeyRequest>
{
    public CreateApiKeyValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Name is required")
            .MaximumLength(100).WithMessage("Name must be 100 characters or less");

        RuleFor(x => x.Scopes)
            .NotNull().WithMessage("Scopes is required");

        RuleFor(x => x.RateLimitRpm)
            .InclusiveBetween(1, 10000)
            .When(x => x.RateLimitRpm.HasValue);

        RuleFor(x => x.AllowedIps)
            .Must(BeValidIpOrCidr)
            .When(x => x.AllowedIps != null && x.AllowedIps.Length > 0);
    }
}

Registrarla es una línea y ASP.NET Core la inyecta en el pipeline automáticamente:

builder.Services.AddValidatorsFromAssemblyContaining<CreateRoleRequestValidator>();

Auth que no tenés que construir a mano

.NET 10 tiene JWT Bearer configurado de fábrica:

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
        Environment.GetEnvironmentVariable("JWT_SECRET") ?? "default-secret"));

    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = Environment.GetEnvironmentVariable("JWT_ISSUER"),
        ValidAudience = Environment.GetEnvironmentVariable("JWT_AUDIENCE"),
        IssuerSigningKey = key,
        ClockSkew = TimeSpan.FromMinutes(5)
    };
});

Issuer, audience, lifetime — todo validado por default.

Observabilidad con Serilog

Serilog se configura una vez y queda:

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .Enrich.FromLogContext()
    .Enrich.WithProperty("Application", "rbac-api")
    .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
    .CreateLogger();

Request logging con contexto enriquecido:

app.UseSerilogRequestLogging(options =>
{
    options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
    {
        diagnosticContext.Set("RequestId", httpContext.TraceIdentifier);
        diagnosticContext.Set("TenantId", httpContext.User.FindFirst("tenant_id")?.Value);
    };
});

OpenTelemetry es una línea:

builder.AddServiceDefaults(); // Health checks, metrics, tracing — todo bundleado

Native AOT: el cold start que te hace reconsiderar serverless

.NET 10 trajo Native AOT: compilás ahead-of-time, sin JIT, sin overhead de compilación en runtime. El cold start pasa de ~500ms a ~50ms.

<PropertyGroup>
  <PublishAot>true</PublishAot>
  <OptimizationPreference>Speed</OptimizationPreference>
</PropertyGroup>

Cierra solo

No voy a decir que .NET 10 es mejor que todo para todos. TypeScript tiene un ecosistema npm enorme. Go gana en herramientas de sistema y CLIs.

Pero cuando tenés un microservicio con lógica de negocio que necesita validación clara, auth correcta, observabilidad que funcione, y tests que prueben contra la realidad — .NET 10 tiene una coherencia que TypeScript y Go no te dan sin pegarte contra varias librerías.

Después de construir la API de RBAC para korz.sh lo que más me sorprendió fue lo poco que tuve que googlear. Validation? El framework tiene. Auth? El framework tiene. Logging y tracing? El framework tiene.

Eso es lo que hace bien .NET 10.


korz.sh — pingedbrain/korz