C#: Construyendo APIs RESTful con ASP.NET Core y Entity Framework Core
Este tutorial te guiará paso a paso en la creación de una API RESTful completa utilizando ASP.NET Core y Entity Framework Core. Exploraremos los fundamentos, la persistencia de datos, la autenticación JWT y las mejores prácticas para construir aplicaciones web modernas y escalables.
🚀 Introducción a las APIs RESTful con ASP.NET Core
En la era de las aplicaciones distribuidas y los microservicios, las APIs RESTful (Representational State Transfer) se han convertido en la columna vertebral de la comunicación entre diferentes sistemas. ASP.NET Core, el framework de Microsoft, nos proporciona una plataforma potente y flexible para construir estas APIs de manera eficiente y escalable.
Este tutorial te sumergirá en el mundo de la construcción de APIs RESTful usando C#, ASP.NET Core y Entity Framework Core. Al final, serás capaz de diseñar, implementar y proteger tus propias APIs, sentando una base sólida para el desarrollo de aplicaciones web y móviles.
¿Qué es una API RESTful? 🤔
Una API RESTful es un conjunto de principios arquitectónicos para diseñar servicios web. Se basa en el protocolo HTTP y utiliza los verbos estándar (GET, POST, PUT, DELETE) para realizar operaciones sobre recursos identificables mediante URIs únicas. Sus características clave incluyen:
- Sin estado (Stateless): Cada solicitud del cliente al servidor contiene toda la información necesaria para entender la solicitud.
- Cliente-Servidor: Separación de preocupaciones entre la interfaz de usuario y el almacenamiento de datos.
- Cacheable: Las respuestas pueden ser cacheables o no cacheables, mejorando el rendimiento.
- Sistema en capas: Permite que un sistema se componga de capas jerárquicas.
- Interfaz Uniforme: La restricción central de REST, incluye identificación de recursos, manipulación de recursos a través de representaciones, mensajes auto-descriptivos y HATEOAS (Hypermedia as the Engine of Application State).
🛠️ Configurando tu Entorno de Desarrollo
Para empezar, necesitamos tener algunas herramientas instaladas en nuestro sistema.
Requisitos Previos 📋
Para seguir este tutorial, necesitarás lo siguiente:
- SDK de .NET: La última versión estable. Puedes descargarlo desde la página oficial de .NET.
- Visual Studio 2022 (con la carga de trabajo 'Desarrollo de ASP.NET y web') o Visual Studio Code (con la extensión C#).
- Un editor de texto (si no usas Visual Studio ni VS Code).
- SQL Server Express o SQLite para la base de datos (usaremos SQLite para simplicidad en este tutorial).
Creando el Proyecto Base ✨
Abriremos una terminal o línea de comandos y crearemos un nuevo proyecto de API web de ASP.NET Core.
dotnet new webapi -n MiApiRest
cd MiApiRest
Este comando creará una carpeta MiApiRest con una estructura de proyecto básica, incluyendo un controlador de ejemplo (WeatherForecastController.cs).
Para verificar que todo funciona, puedes ejecutar el proyecto:
dotnet run
Al acceder a https://localhost:xxxx/swagger (donde xxxx es el puerto asignado), deberías ver la interfaz de Swagger UI con el endpoint de WeatherForecast.
¿Qué es Swagger/OpenAPI? 📖
Swagger (ahora parte de la iniciativa OpenAPI) es un conjunto de herramientas de código abierto que ayuda a diseñar, construir, documentar y consumir APIs RESTful. Genera una interfaz interactiva y visual que describe tu API, permitiendo a los desarrolladores entender y probar los endpoints fácilmente.📂 Estructura del Proyecto y Modelos de Datos
Organizaremos nuestro proyecto para una mejor mantenibilidad y escalabilidad. Seguiremos una estructura que separa las preocupaciones (Domain, Data, Controllers).
Definiendo Nuestro Recurso: Productos 📦
Para este tutorial, construiremos una API para gestionar productos. Cada producto tendrá un Id, Nombre, Precio y Stock.
Crearemos una nueva carpeta llamada Models en la raíz de nuestro proyecto y dentro de ella, el archivo Producto.cs:
// Models/Producto.cs
namespace MiApiRest.Models
{
public class Producto
{
public int Id { get; set; }
public string Nombre { get; set; }
public decimal Precio { get; set; }
public int Stock { get; set; }
}
}
Configurando Entity Framework Core (EF Core) 📊
EF Core es un ORM (Object-Relational Mapper) que nos permite trabajar con bases de datos utilizando objetos .NET, eliminando la necesidad de escribir la mayor parte del código de acceso a datos.
Primero, instalemos los paquetes NuGet necesarios para EF Core y SQLite:
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
Ahora, crearemos una clase de contexto de base de datos en una nueva carpeta Data, llamada ApiDbContext.cs:
// Data/ApiDbContext.cs
using Microsoft.EntityFrameworkCore;
using MiApiRest.Models;
namespace MiApiRest.Data
{
public class ApiDbContext : DbContext
{
public ApiDbContext(DbContextOptions<ApiDbContext> options) : base(options)
{
}
public DbSet<Producto> Productos { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Producto>().HasData(
new Producto { Id = 1, Nombre = "Laptop", Precio = 1200.00m, Stock = 10 },
new Producto { Id = 2, Nombre = "Mouse", Precio = 25.00m, Stock = 50 },
new Producto { Id = 3, Nombre = "Teclado", Precio = 75.00m, Stock = 20 }
);
}
}
}
En Program.cs, debemos registrar nuestro DbContext y configurar la conexión a la base de datos SQLite. Además, aplicaremos las migraciones automáticamente al inicio de la aplicación para simplificar el despliegue en desarrollo.
// Program.cs (fragmento)
using MiApiRest.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Configurar DbContext con SQLite
builder.Services.AddDbContext<ApiDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// Aplicar migraciones al inicio (solo para desarrollo)
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApiDbContext>();
dbContext.Database.Migrate();
}
// ... Resto del código ...
app.Run();
También necesitamos agregar la cadena de conexión en appsettings.json:
// appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "DataSource=MiApiRest.db"
}
}
Migraciones de Entity Framework Core 🔄
Las migraciones permiten evolucionar el esquema de nuestra base de datos a medida que cambia nuestro modelo de datos. Primero, necesitamos inicializar la migración:
dotnet ef migrations add InitialCreate
Luego, aplica esta migración a la base de datos. Como ya configuramos Database.Migrate() en Program.cs, se aplicará automáticamente al iniciar la aplicación. Sin embargo, si quisieras aplicarla manualmente, usarías:
dotnet ef database update
Ahora, al ejecutar la aplicación, se creará el archivo MiApiRest.db y se poblará con los datos iniciales que definimos en OnModelCreating.
⚙️ Creando el Controlador de Productos
Los controladores son el punto de entrada de nuestra API. Manejan las solicitudes HTTP entrantes y devuelven las respuestas.
Implementando el ProductosController 💻
Crearemos un nuevo controlador en la carpeta Controllers llamado ProductosController.cs.
// Controllers/ProductosController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MiApiRest.Data;
using MiApiRest.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MiApiRest.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProductosController : ControllerBase
{
private readonly ApiDbContext _context;
public ProductosController(ApiDbContext context)
{
_context = context;
}
// GET: api/Productos
[HttpGet]
public async Task<ActionResult<IEnumerable<Producto>>> GetProductos()
{
return await _context.Productos.ToListAsync();
}
// GET: api/Productos/5
[HttpGet("{id}")]
public async Task<ActionResult<Producto>> GetProducto(int id)
{
var producto = await _context.Productos.FindAsync(id);
if (producto == null)
{
return NotFound();
}
return producto;
}
// POST: api/Productos
[HttpPost]
public async Task<ActionResult<Producto>> PostProducto(Producto producto)
{
_context.Productos.Add(producto);
await _context.SaveChangesAsync();
return CreatedAtAction("GetProducto", new { id = producto.Id }, producto);
}
// PUT: api/Productos/5
[HttpPut("{id}")]
public async Task<IActionResult> PutProducto(int id, Producto producto)
{
if (id != producto.Id)
{
return BadRequest();
}
_context.Entry(producto).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ProductoExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// DELETE: api/Productos/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProducto(int id)
{
var producto = await _context.Productos.FindAsync(id);
if (producto == null)
{
return NotFound();
}
_context.Productos.Remove(producto);
await _context.SaveChangesAsync();
return NoContent();
}
private bool ProductoExists(int id)
{
return _context.Productos.Any(e => e.Id == id);
}
}
}
Este controlador implementa las operaciones CRUD (Crear, Leer, Actualizar, Eliminar) para nuestros productos:
[HttpGet]y[HttpGet("{id}")]: Para obtener todos los productos y un producto específico por ID, respectivamente.[HttpPost]: Para crear un nuevo producto.[HttpPut("{id}")]: Para actualizar un producto existente.[HttpDelete("{id}")]: Para eliminar un producto.
🔒 Protegiendo Nuestra API con JWT (JSON Web Tokens)
La seguridad es primordial en cualquier API. Implementaremos autenticación basada en JWT para proteger nuestros endpoints.
¿Qué es JWT? 🔑
JWT es un estándar abierto (RFC 7519) que define una forma compacta y autocontenida de transmitir información de forma segura entre las partes como un objeto JSON. Esta información puede ser verificada y confiable porque está firmada digitalmente.
Un JWT consta de tres partes:
- Header: Contiene el tipo de token (JWT) y el algoritmo de firma utilizado (HMAC SHA256 o RSA).
- Payload: Contiene las 'claims' (declaraciones). Las claims son afirmaciones sobre una entidad (generalmente el usuario) y datos adicionales.
- Signature: Se crea codificando el header y el payload con Base64Url y luego firmándolos con una clave secreta y el algoritmo especificado en el header.
Pasos para Implementar JWT 👣
- Instalar paquetes NuGet:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
- Configurar opciones de JWT en
appsettings.json:
// appsettings.json (fragmento)
"Jwt": {
"Key": "estaEsUnaClaveSuperSecretaQueDebeSerLargaYPocoPredecibleParaJWT",
"Issuer": "https://localhost:7001",
"Audience": "https://localhost:7001"
}
<div class="callout important">🔥 <strong>Importante:</strong> La `Key` debe ser una cadena larga y segura. En un entorno de producción, nunca la almacenes directamente en `appsettings.json`; usa variables de entorno o un sistema de gestión de secretos.</div>
3. Crear un modelo para el usuario y credenciales:
Crearemos una clase Usuario y un LoginModel para las credenciales de autenticación.
// Models/Usuario.cs
namespace MiApiRest.Models
{
public class Usuario
{
public int Id { get; set; }
public string Username { get; set; }
public string Password { get; set; } // En un proyecto real, se usaría hashing para contraseñas
public string Rol { get; set; }
}
}
// Models/LoginModel.cs
namespace MiApiRest.Models
{
public class LoginModel
{
public string Username { get; set; }
public string Password { get; set; }
}
}
- Extender
ApiDbContextparaUsuarios:
// Data/ApiDbContext.cs (fragmento)
public DbSet<Usuario> Usuarios { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ... (existing Producto data) ...
modelBuilder.Entity<Usuario>().HasData(
new Usuario { Id = 1, Username = "admin", Password = "password", Rol = "Administrador" },
new Usuario { Id = 2, Username = "user", Password = "password", Rol = "Usuario" }
);
}
Añade una nueva migración para la tabla `Usuarios`:
dotnet ef migrations add AddUsuariosTable
dotnet ef database update
- Configurar la autenticación JWT en
Program.cs:
// Program.cs (fragmento)
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
// ... (existing code) ...
// Configurar Autenticación JWT
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});
builder.Services.AddAuthorization(); // Habilitar la autorización
var app = builder.Build();
// ... (existing code) ...
// Habilitar la autenticación y autorización
app.UseAuthentication();
app.UseAuthorization();
// ... (existing code) ...
- Crear un controlador de autenticación
AuthController.cs: Este controlador manejará las solicitudes de login y generará el JWT.
// Controllers/AuthController.cs
using Microsoft.AspNetCore.Mvc;
using MiApiRest.Data;
using MiApiRest.Models;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.EntityFrameworkCore;
namespace MiApiRest.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly ApiDbContext _context;
private readonly IConfiguration _configuration;
public AuthController(ApiDbContext context, IConfiguration configuration)
{
_context = context;
_configuration = configuration;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginModel model)
{
var user = await _context.Usuarios
.FirstOrDefaultAsync(u => u.Username == model.Username && u.Password == model.Password);
if (user == null)
{
return Unauthorized("Credenciales inválidas");
}
var token = GenerateJwtToken(user);
return Ok(new { token });
}
private string GenerateJwtToken(Usuario user)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.Role, user.Rol),
new Claim("UserId", user.Id.ToString())
};
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.Now.AddMinutes(30), // Token expira en 30 minutos
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
}
- Proteger los endpoints del
ProductosController: Agrega el atributo[Authorize]a nivel de clase o a métodos específicos.
// Controllers/ProductosController.cs (fragmento)
using Microsoft.AspNetCore.Authorization; // Agrega esta línea
namespace MiApiRest.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize] // Protege todo el controlador
public class ProductosController : ControllerBase
{
// ... métodos existentes ...
// Ejemplo de método protegido para roles específicos
[HttpPost]
[Authorize(Roles = "Administrador")] // Solo administradores pueden crear productos
public async Task<ActionResult<Producto>> PostProducto(Producto producto)
{
_context.Productos.Add(producto);
await _context.SaveChangesAsync();
return CreatedAtAction("GetProducto", new { id = producto.Id }, producto);
}
}
}
Ahora, para acceder a los endpoints de Productos, primero tendrás que hacer un POST a /api/Auth/login con tus credenciales (admin/password o user/password) para obtener un token. Luego, incluye este token en el encabezado Authorization de tus solicitudes a /api/Productos con el formato Bearer <tu_token_jwt>.
appsettings.json. Definir la clave, emisor y audiencia.Microsoft.AspNetCore.Authentication.JwtBearer.Program.cs. Configurar TokenValidationParameters.AuthController. Implementar lógica de login y generación de JWT.[Authorize]. Proteger controladores o métodos con el token.📈 Buenas Prácticas y Mejoras Adicionales
Construir una API RESTful no es solo implementar los endpoints, sino también seguir buenas prácticas para asegurar su calidad, rendimiento y mantenibilidad.
Validaciones de Datos 🛡️
Es crucial validar los datos de entrada para evitar errores y proteger la base de datos. ASP.NET Core y C# ofrecen varias formas de hacerlo:
- Data Annotations: Atributos como
[Required],[StringLength],[Range]en tus modelos. - FluentValidation: Una librería popular para crear reglas de validación más complejas y legibles.
Ejemplo con Data Annotations en Producto.cs:
// Models/Producto.cs (fragmento)
using System.ComponentModel.DataAnnotations;
namespace MiApiRest.Models
{
public class Producto
{
public int Id { get; set; }
[Required(ErrorMessage = "El nombre es obligatorio.")]
[StringLength(100, ErrorMessage = "El nombre no puede exceder los 100 caracteres.")]
public string Nombre { get; set; }
[Required(ErrorMessage = "El precio es obligatorio.")]
[Range(0.01, 100000.00, ErrorMessage = "El precio debe estar entre 0.01 y 100000.")]
public decimal Precio { get; set; }
[Required(ErrorMessage = "El stock es obligatorio.")]
[Range(0, 1000, ErrorMessage = "El stock debe estar entre 0 y 1000.")]
public int Stock { get; set; }
}
}
ASP.NET Core maneja automáticamente estas validaciones en los controladores, devolviendo un BadRequest con los errores si el modelo no es válido.
Paginación y Filtrado ➡️⬅️
Para APIs que manejan grandes volúmenes de datos, la paginación y el filtrado son esenciales. Esto mejora el rendimiento al reducir la cantidad de datos transferidos.
Puedes implementar la paginación añadiendo parámetros a tus métodos [HttpGet]:
// Controllers/ProductosController.cs (fragmento)
[HttpGet]
public async Task<ActionResult<IEnumerable<Producto>>> GetProductos(
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string search = null)
{
IQueryable<Producto> query = _context.Productos;
if (!string.IsNullOrEmpty(search))
{
query = query.Where(p => p.Nombre.Contains(search));
}
var productos = await query
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
// Aquí podrías añadir metadatos de paginación a la respuesta (e.g., totalCount, totalPages)
return Ok(productos);
}
Versionado de API 🆙
Es una buena práctica versionar tus APIs para manejar cambios futuros sin romper la compatibilidad con clientes existentes. Las estrategias comunes incluyen:
- Versionado en la URI:
api/v1/productos - Versionado en el encabezado:
X-API-Version: 1.0 - Versionado por Query String:
api/productos?api-version=1.0
Para implementar el versionado en la URI con ASP.NET Core, necesitarías el paquete Microsoft.AspNetCore.Mvc.Versioning.
Intermedio Importante
Manejo Global de Errores 🚫
En lugar de envolver cada método del controlador en bloques try-catch, puedes implementar un manejo global de errores para una experiencia más consistente y limpia.
Esto se puede lograr con UseExceptionHandler o UseStatusCodePages en Program.cs, o creando middleware personalizado. Por ejemplo, una simple implementación para errores HTTP:
// Program.cs (fragmento)
app.UseExceptionHandler("/error"); // Redirige todos los errores a /error
// En Controllers/ErrorController.cs (crear este nuevo controlador)
[Route("/error")]
[ApiController]
public class ErrorController : ControllerBase
{
[AllowAnonymous]
public IActionResult Error()
{
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
// Aquí puedes loggear el error o devolver un mensaje genérico
return Problem(title: context.Error.Message, detail: context.Error.StackTrace);
}
}
Logging y Monitoreo 📈
El logging es fundamental para depurar y monitorear el comportamiento de tu API en producción. ASP.NET Core tiene un sistema de logging integrado que soporta varios proveedores (Console, Debug, EventSource, etc.) y puede extenderse con librerías como Serilog o NLog.
¿Por qué el logging es crucial? 📖
El logging te permite rastrear el flujo de ejecución de tu aplicación, identificar cuellos de botella de rendimiento, diagnosticar errores y comprender cómo interactúan los usuarios con tu API. Un buen sistema de logging es una herramienta invaluable para la observabilidad.🏁 Conclusión
¡Felicidades! 🎉 Has llegado al final de este extenso tutorial sobre cómo construir APIs RESTful con ASP.NET Core y Entity Framework Core. Hemos cubierto desde los fundamentos de REST hasta la implementación de un CRUD completo, la autenticación con JWT y varias buenas prácticas que te ayudarán a crear APIs robustas y escalables.
El desarrollo de APIs es un campo vasto, y este tutorial es solo el comienzo. Te animo a seguir explorando temas como Dockerización, despliegue en la nube (Azure, AWS), pruebas unitarias e integración, y el uso de patrones de diseño avanzados para llevar tus habilidades al siguiente nivel.
Espero que este tutorial te haya proporcionado el conocimiento y la confianza para empezar a construir tus propias APIs RESTful con C# y ASP.NET Core.
¡Feliz codificación! 🚀
Tutoriales relacionados
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!