Como Implementar Versionado de APIs en .NET Core: Mejores Practicas y Guia de Migracion
Aprende a implementar versionado de APIs en .NET Core Web APIs, evitar cambios que rompan compatibilidad, y migrar servicios existentes de forma segura.
El versionado de APIs es crucial para evolucionar tu servicio sin romper clientes existentes. Sin un versionado apropiado, cada cambio se convierte en una negociación con cada consumidor. Con versionado, puedes introducir nuevas características, arreglar diseños pobres y deprecar funcionalidad vieja gradualmente.
En este artículo, cubriremos cómo implementar versionado de APIs en .NET Core usando Asp.Versioning.Mvc, las diferentes estrategias de versionado, mejores prácticas, e integración con Swagger.
Por Qué Versionar Tu API
Sin versionado, cada cambio que rompe compatibilidad requiere coordinar con todos los clientes. Esto se vuelve imposible a escala. El versionado te permite:
- Evolucionar la API independientemente: Agregar nuevas características sin romper clientes existentes.
- Mantener múltiples versiones: Soportar clientes legacy mientras desarrollas nuevas versiones.
- Comunicar cambios claramente: Los clientes saben exactamente qué versión están usando.
- Deprecar apropiadamente: Dar a los clientes tiempo para migrar antes de eliminar versiones viejas.
Configurando Versionado en .NET Core
Primero, instala el paquete NuGet:
dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer
Luego configura versionado en Program.cs:
using Asp.Versioning;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new QueryStringApiVersionReader("api-version"),
new HeaderApiVersionReader("X-Api-Version")
);
}).AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
var app = builder.Build();
app.MapControllers();
app.Run();
Esto configura:
- Versión default:
v1.0 - Asume versión default si no se especifica
- Reporta versiones disponibles en headers de respuesta
- Soporta múltiples estrategias de versionado (URL, query string, header)
Estrategias de Versionado
Hay cuatro formas principales de versionar APIs:
1. URL Path Versioning
La versión es parte de la ruta de URL:
GET /api/v1/users
GET /api/v2/users
Implementación:
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
public class UsersV1Controller : ControllerBase
{
[HttpGet]
public ActionResult<IEnumerable<UserV1Dto>> GetUsers()
{
return Ok(new[] { new UserV1Dto { Id = 1, Name = "John" } });
}
}
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("2.0")]
public class UsersV2Controller : ControllerBase
{
[HttpGet]
public ActionResult<IEnumerable<UserV2Dto>> GetUsers()
{
return Ok(new[] { new UserV2Dto { Id = 1, FullName = "John Doe", Email = "john@example.com" } });
}
}
Ventajas:
- Altamente visible, fácil de entender
- Fácil de testear con navegadores
- Cacheable con CDNs
Desventajas:
- URLs más largas
- Requiere controladores separados o lógica de routing compleja
2. Query String Versioning
La versión se pasa como parámetro de query:
GET /api/users?api-version=1.0
GET /api/users?api-version=2.0
Implementación:
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet]
[ApiVersion("1.0")]
public ActionResult<IEnumerable<UserV1Dto>> GetUsersV1()
{
return Ok(new[] { new UserV1Dto { Id = 1, Name = "John" } });
}
[HttpGet]
[ApiVersion("2.0")]
public ActionResult<IEnumerable<UserV2Dto>> GetUsersV2()
{
return Ok(new[] { new UserV2Dto { Id = 1, FullName = "John Doe", Email = "john@example.com" } });
}
}
Configura el lector en Program.cs:
options.ApiVersionReader = new QueryStringApiVersionReader("api-version");
Ventajas:
- No cambia la estructura de URL
- Fácil de agregar a requests existentes
Desventajas:
- Query strings menos prominentes
- Puede complicar caching
3. Header Versioning
La versión se especifica en un header HTTP personalizado:
GET /api/users
X-Api-Version: 1.0
Implementación:
options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
El resto del código permanece igual.
Ventajas:
- URL limpia
- Sigue principios RESTful (mismo recurso, diferentes representaciones)
Desventajas:
- Más difícil de testear con navegadores
- Los headers son menos visibles
4. Media Type Versioning (Content Negotiation)
La versión se especifica en el header Accept:
GET /api/users
Accept: application/json;v=1.0
Implementación:
options.ApiVersionReader = new MediaTypeApiVersionReader();
Ventajas:
- Enfoque RESTful puro
- URL limpia
Desventajas:
- Más complejo de implementar y testear
- Menos soporte de herramientas
Versionado a Nivel de Acción
Puedes versionar actions individuales dentro de un controlador:
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet]
[ApiVersion("1.0")]
public ActionResult<IEnumerable<UserV1Dto>> GetUsersV1()
{
return Ok(new[] { new UserV1Dto { Id = 1, Name = "John" } });
}
[HttpGet]
[ApiVersion("2.0")]
public ActionResult<IEnumerable<UserV2Dto>> GetUsersV2()
{
return Ok(new[] { new UserV2Dto { Id = 1, FullName = "John Doe", Email = "john@example.com" } });
}
[HttpGet("{id}")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public ActionResult<UserV2Dto> GetUserById(int id)
{
// Mismo endpoint para ambas versiones
return Ok(new UserV2Dto { Id = id, FullName = "John Doe", Email = "john@example.com" });
}
}
Deprecando Versiones
Marca versiones como deprecated para advertir a los clientes:
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
public class UsersController : ControllerBase
{
// ...
}
Los clientes recibirán un header de respuesta:
api-deprecated-versions: 1.0
api-supported-versions: 2.0
Integración con Swagger
Para documentar múltiples versiones en Swagger:
dotnet add package Swashbuckle.AspNetCore
Configura Swagger en Program.cs:
using Microsoft.OpenApi.Models;
using Asp.Versioning.ApiExplorer;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApiVersioning(/* ... */).AddApiExplorer(/* ... */);
builder.Services.AddSwaggerGen(options =>
{
var provider = builder.Services.BuildServiceProvider()
.GetRequiredService<IApiVersionDescriptionProvider>();
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, new OpenApiInfo
{
Title = $"My API {description.ApiVersion}",
Version = description.ApiVersion.ToString()
});
}
});
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI(options =>
{
var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint(
$"/swagger/{description.GroupName}/swagger.json",
description.GroupName.ToUpperInvariant()
);
}
});
app.MapControllers();
app.Run();
Ahora Swagger mostrará un dropdown para seleccionar versiones de API.
Mejores Prácticas
1. Usa Semantic Versioning
- Major version (v1, v2): Cambios que rompen compatibilidad
- Minor version (v1.1, v1.2): Características retrocompatibles
- Patch version (v1.1.1): Bug fixes
2. Solo Versiona Cuando Sea Necesario
No todo cambio requiere una nueva versión. Estos son retrocompatibles:
- Agregar nuevos endpoints
- Agregar campos opcionales a requests
- Agregar campos a responses
Estos rompen compatibilidad:
- Cambiar o remover campos existentes
- Cambiar tipos de datos
- Cambiar comportamiento de endpoints existentes
3. Mantén Consistencia
Elige una estrategia de versionado y mantente con ella. No mezcles URL versioning y header versioning.
4. Documenta Cambios
Mantén un changelog para cada versión. Comunica claramente:
- Qué cambió
- Por qué cambió
- Cómo migrar
5. Da Avisos de Deprecación Adecuados
No elimines versiones abruptamente. Provee:
- Al menos 6 meses de aviso
- Guías de migración claras
- Headers
api-deprecated-versions
Ejemplo Completo: Migrando de v1 a v2
v1:
public class UserV1Dto
{
public int Id { get; set; }
public string Name { get; set; }
}
[ApiVersion("1.0", Deprecated = true)]
[Route("api/v{version:apiVersion}/users")]
public class UsersV1Controller : ControllerBase
{
[HttpGet]
public ActionResult<IEnumerable<UserV1Dto>> GetUsers()
{
return Ok(new[] { new UserV1Dto { Id = 1, Name = "John Doe" } });
}
}
v2 (DTO mejorado):
public class UserV2Dto
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/users")]
public class UsersV2Controller : ControllerBase
{
[HttpGet]
public ActionResult<IEnumerable<UserV2Dto>> GetUsers()
{
return Ok(new[]
{
new UserV2Dto
{
Id = 1,
FirstName = "John",
LastName = "Doe",
Email = "john@example.com"
}
});
}
}
Los clientes pueden migrar gradualmente de v1 a v2, y eventualmente deprecas y remueves v1.
Conclusión
El versionado de APIs es esencial para mantener APIs a largo plazo. En .NET Core, Asp.Versioning.Mvc hace fácil implementar cualquiera de las estrategias comunes:
- URL path versioning: Mejor para APIs públicas (más visible)
- Header versioning: Mejor para APIs internas (URLs limpias)
- Query string: Compromiso razonable
Recuerda:
- Versiona solo cuando rompes compatibilidad
- Depreca apropiadamente antes de remover
- Documenta todos los cambios
- Usa Swagger para generar documentación por versión
Con el versionado correcto, tu API puede evolucionar sin dejar clientes atrás.