Documentación y Despliegue
El último módulo esencial se centra en la documentación para desarrolladores (usando Swagger/OpenAPI) y en la preparación para llevar tu aplicación a un entorno de producción.
6.1. Documentación con OpenAPI (Swagger)
OpenAPI (antes conocido como Swagger) es una especificación estándar para describir APIs RESTful. ASP.NET Core tiene un excelente soporte integrado para generar documentación de forma automática.
Configuración en Minimal APIs
Para integrar Swagger UI, necesitas dos paquetes de Nuget: Microsoft.AspNetCore.OpenApi (que ya viene incluido en los templates de Minimal API) y Swashbuckle.AspNetCore.
Pasos Clave:
- Registro de Servicios: Habilita el explorador de endpoints y el generador de Swagger.
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); - Uso del Middleware: Habilita la generación del archivo
swagger.jsony la interfaz de usuario web interactiva.if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); // Permite acceder a la interfaz en /swagger }
Una vez configurado, puedes acceder a la interfaz de documentación en la ruta /swagger.
6.2. Mejorando la Documentación del Endpoint
Para que la documentación de Swagger sea útil, debemos añadir metadata a nuestros endpoints:
| Método de Extensión | Propósito |
|---|---|
.WithName("NombreAccion") |
Asigna un nombre único para generar el código cliente. |
.WithTags("NombreGrupo") |
Agrupa endpoints en categorías en Swagger UI. |
.WithDescription("Detalle") |
Añade una descripción larga a la acción. |
.Produces<T>(statusCode) |
Documenta el tipo de respuesta (ej. .Produces<TodoItem>(200)). |
.ExcludeFromDescription() |
Oculta el endpoint de la documentación de Swagger. |
6.3. Despliegue (Deployment)
El despliegue es el proceso de llevar tu código a un servidor donde esté accesible públicamente.
Opciones Comunes
- Auto-alojamiento (Self-Hosting): Desplegar directamente el ejecutable de .NET en un servidor Linux (usando Kestrel detrás de Nginx o Apache).
- Contenedores (Docker): El método moderno preferido. Empaquetas tu aplicación con todas sus dependencias en un contenedor. Esto asegura que la aplicación se ejecute de manera idéntica en cualquier entorno.
- Plataformas Cloud: Usar servicios gestionados que abstraen el servidor, como:
- Azure App Services o Azure Container Apps (Microsoft)
- AWS Elastic Beanstalk o AWS Fargate (Amazon)
- Google Cloud Run o Google App Engine (Google)
Compilación y Publicación
Antes de desplegar, debes publicar tu aplicación. Esto crea una versión optimizada y lista para producción.
dotnet publish -c Release -o /app/publish
El comando -c Release asegura que se compile en modo Release (optimizado). El resultado es un directorio con el ejecutable y las bibliotecas necesarias.
// Usings necesarios.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System.Security.Claims;
// --------------------------------------------------------------------
// NUEVOS USINGS PARA DOCUMENTACIÓN (MÓDULO 6)
// --------------------------------------------------------------------
using Microsoft.OpenApi.Models; // Para configurar Swagger
// ====================================================================
// MODELOS Y CONTEXTO DE DATOS (MÓDULO 4 - MANTENIDOS)
// ====================================================================
public class TodoItem
{
public int Id { get; set; }
[Required(ErrorMessage = "El título es obligatorio.")]
[StringLength(100, ErrorMessage = "El título no puede exceder los 100 caracteres.")]
public string Title { get; set; } = string.Empty;
public bool IsComplete { get; set; }
}
public class ApiDbContext : DbContext
{
public ApiDbContext(DbContextOptions<ApiDbContext> options) : base(options) { }
public DbSet<TodoItem> Todos => Set<TodoItem>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TodoItem>().HasData(
new TodoItem { Id = 1, Title = "Completar Módulo 6 (Documentación)", IsComplete = false },
new TodoItem { Id = 2, Title = "Revisar Despliegue", IsComplete = true }
);
}
}
// ====================================================================
// MODELOS DE CONFIGURACIÓN Y SERVICIOS (MÓDULO 3 - MANTENIDOS)
// ====================================================================
public class SettingsOptions
{
public const string Settings = "Settings";
public string NombreAplicacion { get; set; } = "API por Defecto";
public int MaxItems { get; set; }
}
public interface IItemService
{
List<string> ObtenerDatos();
}
public class MockItemService : IItemService
{
private readonly List<string> _items = new List<string> { "Item A", "Item B", "Item C" };
public List<string> ObtenerDatos() => _items;
}
// ====================================================================
// APLICACIÓN PRINCIPAL (Program.cs)
// ====================================================================
var builder = WebApplication.CreateBuilder(args);
// --- CLAVE SECRETA PARA JWT (MÓDULO 5) ---
var jwtKey = builder.Configuration["Jwt:Key"] ?? "EstaEsUnaClaveSuperSecretaDePruebaYDebeTenerMasDe16Caracteres";
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
// --------------------------------------------------------------------
// MÓDULO 6: CONFIGURACIÓN DE SWAGGER/OPENAPI
// --------------------------------------------------------------------
// 6.1. Registro de servicios para el explorador de endpoints (necesario para Swagger)
builder.Services.AddEndpointsApiExplorer();
// 6.1. Registro de servicios para Swagger
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Minimal API Demo", Version = "v1" });
// Configuración para permitir la autenticación JWT en Swagger UI
var securityScheme = new OpenApiSecurityScheme
{
Name = "JWT Authentication",
Description = "Ingresa el token JWT en formato 'Bearer {token}'",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Scheme = "bearer", // La palabra clave del esquema
BearerFormat = "JWT",
Reference = new OpenApiReference
{
Id = JwtBearerDefaults.AuthenticationScheme,
Type = ReferenceType.SecurityScheme
}
};
c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme);
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{ securityScheme, Array.Empty<string>() }
});
});
// --------------------------------------------------------------------
// MÓDULO 5: REGISTRO DE MIDDLEWARE Y SEGURIDAD (MANTENIDOS)
// --------------------------------------------------------------------
// 5.2. Configuración de CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
// 5.3. Configuración de Autenticación (JWT Bearer)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = securityKey
};
});
// 5.4. Configuración de Autorización
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminPolicy", policy =>
policy.RequireClaim(ClaimTypes.Role, "Admin"));
});
// --------------------------------------------------------------------
// REGISTRO DE PERSISTENCIA Y SERVICIOS (MÓDULO 4 - MANTENIDOS)
// --------------------------------------------------------------------
builder.Services.AddDbContext<ApiDbContext>(options =>
options.UseInMemoryDatabase("MinimalDb"));
builder.Services.AddSingleton<IItemService, MockItemService>();
builder.Services.Configure<SettingsOptions>(
builder.Configuration.GetSection(SettingsOptions.Settings));
builder.Services.AddTransient<OtroServicio>();
var app = builder.Build();
// Inicialización de la base de datos en memoria (opcional pero útil)
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<ApiDbContext>();
db.Database.EnsureCreated();
}
// --------------------------------------------------------------------
// 5.1. PIPELINE DE MIDDLEWARE: INCLUSIÓN DE SWAGGER (MÓDULO 6)
// --------------------------------------------------------------------
if (app.Environment.IsDevelopment())
{
// 6.1. Middleware de Swagger UI
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Minimal API Demo V1");
// Opcional: Establece la página de inicio al abrir la URL base.
c.RoutePrefix = string.Empty;
});
app.UseDeveloperExceptionPage();
}
app.UseRouting();
// CORS, Autenticación y Autorización siguen el orden estándar
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseAuthorization();
// --------------------------------------------------------------------
// ENDPOINTS (MÓDULOS 3, 4, 5 - MANTENIDOS Y MEJORADOS)
// --------------------------------------------------------------------
// MÓDULO 3 ENDPOINTS
app.MapGet("/api/v1/items-con-servicio", (IItemService itemService) =>
{
var datos = itemService.ObtenerDatos();
return Results.Ok(new { Mensaje = "Datos obtenidos del servicio inyectado", Datos = datos });
})
.WithName("ItemsConServicio")
.WithTags("Servicios Mock"); // <--- Añadido para agrupar en Swagger
// Endpoint de configuración
app.MapGet("/api/v1/configuracion", (IOptions<SettingsOptions> options) =>
{
var settings = options.Value;
return Results.Ok(new
{
settings.NombreAplicacion,
settings.MaxItems,
Mensaje = "Opciones de configuración inyectadas correctamente."
});
})
.WithName("Configuracion")
.WithTags("Servicios Mock") // <--- Añadido para agrupar en Swagger
.Produces<SettingsOptions>(200); // <--- Documenta la respuesta
app.MapDemoGroups(); // Llama al grupo de rutas V2
// MÓDULO 4/5: ENDPOINTS CRUD CON PERSISTENCIA, VALIDACIÓN Y SEGURIDAD
var todoApi = app.MapGroup("/api/v1/todos")
.WithTags("ToDos (CRUD con Seguridad)");
// GET: Abierto
todoApi.MapGet("/", async (ApiDbContext db) =>
await db.Todos.ToListAsync())
.WithName("GetTodos")
.Produces<List<TodoItem>>(200);
// GET por ID: Abierto
todoApi.MapGet("/{id:int}", async (int id, ApiDbContext db) =>
await db.Todos.FindAsync(id)
is TodoItem todo
? Results.Ok(todo)
: Results.NotFound())
.WithName("GetTodoById")
.Produces<TodoItem>(200)
.Produces(404);
// POST: Requiere Autenticación
todoApi.MapPost("/", async (TodoItem todo, ApiDbContext db) =>
{
if (string.IsNullOrWhiteSpace(todo.Title) || todo.Title.Length > 100)
{
return Results.BadRequest(new { Error = "Validación fallida para 'Title'." });
}
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/api/v1/todos/{todo.Id}", todo);
})
.RequireAuthorization()
.WithName("CreateTodo")
.Produces<TodoItem>(201)
.Produces(400)
.WithDescription("Crea una nueva tarea. Requiere autenticación.");
// DELETE: Requiere Política "AdminPolicy"
todoApi.MapDelete("/{id:int}", async (int id, ApiDbContext db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
})
.RequireAuthorization("AdminPolicy")
.WithName("DeleteTodo")
.Produces(204)
.Produces(404)
.Produces(403); // Añadido para documentar la restricción por rol
// --- Endpoint de SIMULACIÓN DE LOGIN para obtener un token de prueba ---
app.MapPost("/api/login", (LoginRequest request) =>
{
// Lógica de validación
if (request.Username != "testuser" || request.Password != "password")
{
return Results.Unauthorized();
}
// 1. Definir Claims (Identidad y Roles)
var claims = new[]
{
new Claim(ClaimTypes.Name, request.Username),
new Claim(ClaimTypes.Email, $"{request.Username}@test.com"),
new Claim(ClaimTypes.Role, "Admin")
};
// 2. Crear Token
var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(
claims: claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256)
);
var tokenHandler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
var tokenString = tokenHandler.WriteToken(token);
return Results.Ok(new { token = tokenString, message = "Token generado. Úsalo como 'Bearer <token>' para los endpoints restringidos." });
})
.WithTags("Autenticación")
.WithDescription("Obtiene un token JWT de prueba (usuario: testuser, pass: password).");
public record LoginRequest(string Username, string Password);
// --------------------------------------------------------------------
// EXTENSIÓN PARA AGRUPACIÓN Y MANEJO DE ENDPOINTS FUERA DE Program.cs (MÓDULO 3 - MANTENIDOS)
// --------------------------------------------------------------------
public class OtroServicio
{
public string ObtenerInfo() => "Información de un servicio del grupo de rutas.";
}
public static class RouteGroupExtensions
{
public static void MapDemoGroups(this WebApplication app)
{
var grupoV2 = app.MapGroup("/api/v2/grupo")
.WithTags("Grupo V2 (Agrupación)");
grupoV2.MapGet("/saludo", () =>
{
return Results.Ok("¡Hola desde el Grupo V2!");
})
.WithName("GrupoV2Saludo");
grupoV2.MapGet("/info", (OtroServicio otroServicio) =>
{
return Results.Ok(new { Mensaje = otroServicio.ObtenerInfo() });
})
.WithName("GrupoV2Info");
}
}
app.Run();