Descripción de la imagen
Módulo 1 · Guía del instructor y handout

Construcción de aplicaciones con .NET Core

Desarrollo ampliado de los Conceptos Clave — 7 subtemas con explicación técnica, ejemplos de código, casos de uso, errores comunes y buenas prácticas.

1

Arquitectura y características de .NET Core

1.1.1. Evolución histórica y unificación

1.1.2. Componentes del runtime

ComponenteRol
CoreCLREjecuta IL, GC, JIT, interoperabilidad
BCL (Base Class Library)Tipos base: System.*, colecciones, IO, HTTP
RoslynCompilador de C#/VB.NET
MSBuildSistema de build
NuGetGestor de paquetes
KestrelServidor web cross-platform
ASP.NET CoreFramework web (MVC, Minimal APIs, SignalR, gRPC)
EF CoreORM

1.1.3. SDK vs Runtime

1.1.4. Modelos de despliegue

ModeloRequiere runtime instaladoTamañoIdeal para
Framework-dependentSíPequeñoServidores con runtime compartido
Self-containedNoGrande (~70 MB)Contenedores, edge
Single-fileDependeMedioDistribución simple
Native AOTNoMuy pequeño, arranque rápidoServerless, CLI, microservicios
# Publicación self-contained
dotnet publish -c Release -r linux-x64 --self-contained true /p:PublishSingleFile=true

1.1.5. Ventajas frente a .NET Framework

⚠ Errores comunes
  • Confundir .NET Core con .NET Framework en dependencias legacy.
  • Instalar solo el runtime y esperar compilar.
  • Mezclar target frameworks en un mismo proyecto.
2

Estructura de una aplicación

1.2.1. Archivo .csproj

Es el corazón del proyecto. Define target framework, dependencias, propiedades y tareas de build.

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <InvariantGlobalization>true</InvariantGlobalization>
    <UserSecretsId>demo-app-secrets</UserSecretsId>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
  </ItemGroup>
</Project>

Diferencias por SDK:

1.2.2. Program.cs y top-level statements

Desde C# 9, no hay Main explícito. El compilador genera la clase Program con el método Main.

var builder = WebApplication.CreateBuilder(args);

// 1. Servicios
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// 2. Pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapControllers();
app.Run();

1.2.3. launchSettings.json

Define perfiles de depuración: puertos, variables de entorno, URL de lanzamiento.

{
  "profiles": {
    "http": {
      "commandName": "Project",
      "applicationUrl": "http://localhost:5100",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

1.2.4. Convenciones de carpetas

CarpetaPropósito
Controllers/Controladores MVC/API
Services/Lógica de negocio + interfaces
Models/DTOs, entidades, ViewModels
Data/DbContext, repositorios
Middleware/Middlewares personalizados
Extensions/Métodos de extensión (DI, config)
Options/Clases de configuración tipada

1.2.5. Archivos generados (no versionar)

⚠ Errores comunes
  • Editar bin/ u obj/ creyendo que son fuente.
  • No versionar appsettings.json (o peor, versionar secretos).
  • Duplicar lógica en Program.cs en lugar de extraer a extensiones.
3

Configuración y gestión de entornos

1.3.1. Fuentes de configuración y precedencia

El orden importa: la última fuente sobrescribe las anteriores.

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. User Secrets (solo Development)
  4. Variables de entorno
  5. Argumentos de línea de comandos
  6. (Opcional) Azure Key Vault, Consul, etc.
builder.Configuration
    .AddJsonFile("appsettings.json", optional: false)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
    .AddEnvironmentVariables()
    .AddCommandLine(args);

1.3.2. Entornos (IWebHostEnvironment)

if (app.Environment.IsDevelopment()) { /* ... */ }
if (app.Environment.IsProduction())  { /* ... */ }

Se define con ASPNETCORE_ENVIRONMENT o DOTNET_ENVIRONMENT.

1.3.3. Patrón Options

Tres variantes:

InterfazCicloUso
IOptions<T>SingletonConfiguración estática
IOptionsSnapshot<T>ScopedRelee en cada request
IOptionsMonitor<T>SingletonRelee + notifica cambios
public class EmailSettings
{
    public string Host { get; set; } = string.Empty;
    public int Port { get; set; }
    public bool UseSsl { get; set; }
}

builder.Services.Configure<EmailSettings>(
    builder.Configuration.GetSection("Email"));

// Consumo
public class EmailService
{
    private readonly EmailSettings _settings;
    public EmailService(IOptions<EmailSettings> options) => _settings = options.Value;
}

1.3.4. Validación de configuración

builder.Services.AddOptions<EmailSettings>()
    .Bind(builder.Configuration.GetSection("Email"))
    .ValidateDataAnnotations()
    .ValidateOnStart();

1.3.5. User Secrets

Ideal para desarrollo local: almacena fuera del repo.

dotnet user-secrets init
dotnet user-secrets set "Email:Password" "s3cr3t"
dotnet user-secrets list
⚠ Errores comunes
  • Dejar secretos en appsettings.json versionado.
  • Usar IConfiguration["..."] en lugar de Options tipadas.
  • Ignorar que las variables de entorno usan __ (doble guion bajo) para jerarquía: Email__Host.
4

Inyección de dependencias

1.4.1. ¿Por qué DI?

1.4.2. Contenedor nativo

public interface IEmailSender { Task SendAsync(string to, string body); }

public class SmtpEmailSender : IEmailSender
{
    private readonly EmailSettings _settings;
    public SmtpEmailSender(IOptions<EmailSettings> opts) => _settings = opts.Value;
    public Task SendAsync(string to, string body) { /* ... */ return Task.CompletedTask; }
}

builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();

1.4.3. Ciclos de vida (¡crítico!)

CicloInstanciaCuándo usar
TransientNueva siempreServicios ligeros sin estado
ScopedUna por requestDbContext, unit of work
SingletonUna por appCachés, configuradores, HttpClient factory
builder.Services.AddTransient<IHasher, Sha256Hasher>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<IClock, SystemClock>();

1.4.4. Captive Dependency (error clásico)

Inyectar un Scoped dentro de un Singleton deja capturada la instancia para toda la vida de la app.

// ❌ MAL
builder.Services.AddSingleton<BadService>(); // depende de DbContext (Scoped)

// ✅ BIEN: usar IServiceScopeFactory
public class GoodService
{
    private readonly IServiceScopeFactory _scopeFactory;
    public GoodService(IServiceScopeFactory sf) => _scopeFactory = sf;

    public void DoWork()
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        // ...
    }
}

1.4.5. Registro avanzado

// Múltiples implementaciones
builder.Services.AddScoped<INotifier, EmailNotifier>();
builder.Services.AddScoped<INotifier, SmsNotifier>();

// Resolver todas
public class Dispatcher(IEnumerable<INotifier> notifiers) { }

// Factory delegate
builder.Services.AddScoped<IPaymentProcessor>(sp =>
{
    var cfg = sp.GetRequiredService<IConfiguration>();
    return cfg["Payments:Provider"] switch
    {
        "Stripe" => new StripeProcessor(),
        "PayPal" => new PayPalProcessor(),
        _ => throw new InvalidOperationException()
    };
});
⚠ Errores comunes
  • Registrar todo como Singleton "por rendimiento".
  • No usar interfaces (imposibilita mocks).
  • Resolver servicios con GetService dentro de constructores.
  • Olvidar que AddDbContext es Scoped por defecto.
5

Middleware y pipeline

1.5.1. ¿Qué es el pipeline?

Es una cadena de delegados que procesan HttpContext. Cada middleware puede:

Request  → [MW1] → [MW2] → [MW3] → Endpoint
Response ← [MW1] ← [MW2] ← [MW3] ← Endpoint

1.5.2. Orden canónico recomendado

app.UseExceptionHandler();      // 1. Captura todo
app.UseHsts();                  // 2. Seguridad
app.UseHttpsRedirection();      // 3. HTTPS
app.UseStaticFiles();           // 4. Archivos estáticos
app.UseRouting();               // 5. Routing
app.UseCors();                  // 6. CORS
app.UseAuthentication();        // 7. ¿Quién eres?
app.UseAuthorization();         // 8. ¿Puedes?
app.UseRateLimiter();           // 9. Throttling
app.MapControllers();           // 10. Endpoints

1.5.3. Tres formas de middleware

// 1. Inline terminal (corta la cadena)
app.Run(async ctx => await ctx.Response.WriteAsync("Hola"));

// 2. Inline con next
app.Use(async (ctx, next) =>
{
    Console.WriteLine($"→ {ctx.Request.Path}");
    await next();
    Console.WriteLine($"← {ctx.Response.StatusCode}");
});

// 3. Clase (recomendado para reutilización)
public class TimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<TimingMiddleware> _logger;

    public TimingMiddleware(RequestDelegate next, ILogger<TimingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext ctx)
    {
        var sw = Stopwatch.StartNew();
        try { await _next(ctx); }
        finally
        {
            _logger.LogInformation("{Method} {Path} {Status} en {Ms}ms",
                ctx.Request.Method, ctx.Request.Path,
                ctx.Response.StatusCode, sw.ElapsedMilliseconds);
        }
    }
}

app.UseMiddleware<TimingMiddleware>();

1.5.4. Middleware condicional

app.UseWhen(
    ctx => ctx.Request.Path.StartsWithSegments("/api"),
    branch => branch.UseMiddleware<ApiKeyMiddleware>());

1.5.5. Map para ramificar

app.Map("/admin", adminApp =>
{
    adminApp.UseMiddleware<AdminAuthMiddleware>();
    adminApp.Run(async ctx => await ctx.Response.WriteAsync("Admin"));
});
⚠ Errores comunes
  • Llamar UseRouting después de UseAuthorization.
  • Registrar UseAuthentication después de UseAuthorization.
  • Middlewares costosos antes de los de seguridad.
  • No llamar a next() y romper el pipeline.
6

Routing y componentes de aplicación

1.6.1. Routing basado en atributos (Controllers)

[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class OrdersController : ControllerBase
{
    [HttpGet]                                    // GET /api/orders
    public IActionResult GetAll([FromQuery] int page = 1) => Ok();

    [HttpGet("{id:int:min(1)}")]                 // GET /api/orders/5
    public IActionResult GetById(int id) => Ok();

    [HttpGet("{slug:alpha}")]                    // GET /api/orders/abc
    public IActionResult GetBySlug(string slug) => Ok();

    [HttpPost]                                   // POST /api/orders
    public IActionResult Create([FromBody] OrderDto dto) => Created("", dto);

    [HttpPut("{id}")]
    public IActionResult Update(int id, [FromBody] OrderDto dto) => NoContent();

    [HttpDelete("{id}")]
    public IActionResult Delete(int id) => NoContent();
}

1.6.2. Restricciones de ruta (route constraints)

int, bool, datetime, decimal, guid, length(n), min(n), max(n), range(a,b), alpha, regex(...).

1.6.3. Minimal APIs

var products = app.MapGroup("/api/products")
                  .WithTags("Products")
                  .RequireAuthorization();

products.MapGet("/", (IProductService svc) => Results.Ok(svc.GetAll()))
        .WithName("GetProducts")
        .Produces<IEnumerable<Product>>(200);

products.MapGet("/{id:int}", (int id, IProductService svc) =>
{
    var p = svc.Get(id);
    return p is null ? Results.NotFound() : Results.Ok(p);
});

products.MapPost("/", (Product p, IProductService svc) =>
{
    svc.Add(p);
    return Results.Created($"/api/products/{p.Id}", p);
})
.WithParameterValidation();

1.6.4. Controllers vs Minimal APIs

AspectoControllersMinimal APIs
VerbosidadAltaBaja
Ideal paraAPIs grandes, MVCMicroservicios, endpoints pequeños
FiltrosIActionFilter, etc.IEndpointFilter
Model bindingAutomáticoAutomático (parámetros)
ValidaciónDataAnnotationsDataAnnotations + manual

1.6.5. Endpoint metadata

app.MapGet("/secret", () => "ok")
   .WithName("GetSecret")
   .WithTags("Admin")
   .RequireAuthorization("AdminOnly")
   .WithOpenApi();
⚠ Errores comunes
  • Rutas ambiguas (/api/orders/{id} vs /api/orders/{name}).
  • No versionar la API (/api/v1/... o Asp.Versioning).
  • Ignorar el orden de rutas específicas antes que genéricas.
7

Gestión de errores y logging

1.7.1. Niveles de log

NivelCuándo usar
TraceDetalles internos muy verbosos
DebugDiagnóstico en desarrollo
InformationEventos normales de negocio
WarningAlgo anómalo pero recuperable
ErrorFallo en operación actual
CriticalFallo catastrófico (app caída)
NoneDesactiva logs

1.7.2. Logging estructurado

// ✅ Correcto: placeholders → propiedades buscables
_logger.LogInformation("Orden {OrderId} creada por {UserId} por {Amount:C}",
    order.Id, order.UserId, order.Total);

// ❌ Incorrecto: concatenación → no estructurado
_logger.LogInformation($"Orden {order.Id} creada");

1.7.3. Configuración de logging en appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
    }
  }
}

1.7.4. Manejo global de excepciones

Opción A: UseExceptionHandler con lambda

app.UseExceptionHandler(errApp =>
{
    errApp.Run(async ctx =>
    {
        var feature = ctx.Features.Get<IExceptionHandlerFeature>();
        ctx.Response.StatusCode = 500;
        await ctx.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Title = "Error interno",
            Detail = feature?.Error.Message
        });
    });
});

Opción B: IExceptionHandler (.NET 8+)

public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;
    private readonly IHostEnvironment _env;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger, IHostEnvironment env)
    {
        _logger = logger;
        _env = env;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext ctx, Exception ex, CancellationToken ct)
    {
        _logger.LogError(ex, "Excepción no controlada: {Message}", ex.Message);
        ctx.Response.StatusCode = ex switch
        {
            NotFoundException => StatusCodes.Status404NotFound,
            ValidationException => StatusCodes.Status400BadRequest,
            _ => StatusCodes.Status500InternalServerError
        };

        await ctx.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = ctx.Response.StatusCode,
            Title = "Error procesando la solicitud",
            Detail = _env.IsDevelopment() ? ex.ToString() : "Contacte al administrador",
            Instance = ctx.Request.Path
        }, ct);

        return true;
    }
}

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler();

1.7.5. ProblemDetails (RFC 7807)

Estándar para errores en APIs:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "detail": "El campo Name es requerido.",
  "instance": "/api/products",
  "traceId": "00-abc...-01"
}

1.7.6. Excepciones personalizadas + middleware

public class NotFoundException : Exception
{
    public NotFoundException(string msg) : base(msg) { }
}

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ErrorHandlingMiddleware> _logger;

    public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
    {
        _next = next; _logger = logger;
    }

    public async Task InvokeAsync(HttpContext ctx)
    {
        try { await _next(ctx); }
        catch (NotFoundException ex)
        {
            _logger.LogWarning(ex, "Recurso no encontrado");
            ctx.Response.StatusCode = 404;
            await ctx.Response.WriteAsJsonAsync(new { error = ex.Message });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error no controlado");
            ctx.Response.StatusCode = 500;
            await ctx.Response.WriteAsJsonAsync(new { error = "Error interno" });
        }
    }
}

1.7.7. Serilog (logging a archivos / sinks)

builder.Host.UseSerilog((ctx, cfg) =>
    cfg.ReadFrom.Configuration(ctx.Configuration)
       .Enrich.FromLogContext()
       .WriteTo.Console()
       .WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day));

1.7.8. Correlación de requests (traceId)

app.Use(async (ctx, next) =>
{
    var traceId = Activity.Current?.Id ?? ctx.TraceIdentifier;
    ctx.Response.Headers["X-Trace-Id"] = traceId;
    using (_logger.BeginScope(new Dictionary<string, object> { ["TraceId"] = traceId }))
        await next();
});
⚠ Errores comunes
  • Loguear datos sensibles (contraseñas, tarjetas, PII).
  • catch (Exception) { } silencioso.
  • Devolver stack traces en producción.
  • Un solo try/catch gigante en Program.cs.
  • Loguear como Error lo que es Warning (y viceversa).