M4 ‐ Avanzado: Técnicas y Herramientas Complementarias - sjperalta/Web-Service-C-sharp GitHub Wiki

Módulo 4: Avanzado: Técnicas y Herramientas Complementarias

Versionado de Web Services

Estrategias para Versionar Servicios:

  1. Versionado en la URL:

    • Se incluye la versión en la URL del servicio.
    • Ejemplo:
      /api/v1/products
      /api/v2/products
      
  2. Versionado en el Header:

    • Se utiliza un header HTTP para especificar la versión.
    • Ejemplo:
      GET /api/products
      Accept: application/vnd.myapi.v1+json
  3. Versionado en Parámetros de Consulta:

    • Se pasa la versión como un parámetro en la consulta.
    • Ejemplo:
      /api/products?version=1
      

Manejo de Cambios y Compatibilidad:

  • Backward Compatibility: Mantener la compatibilidad con versiones anteriores tanto como sea posible.
  • Deprecación Controlada: Informar a los usuarios sobre versiones que serán obsoletas, dando tiempo suficiente para la migración.
  • Documentación Completa: Proveer documentación clara sobre cambios en nuevas versiones y cómo migrar a ellas.

Ejemplo

Implementar el versionado de API en .NET Core 8 implica algunos pasos. A continuación, te guiaré sobre cómo configurar el versionado de API en un proyecto de .NET Core 8.

1. Instalar los Paquetes Necesarios

Primero, necesitas instalar los paquetes NuGet necesarios. Puedes hacerlo a través del Administrador de Paquetes NuGet o usando la CLI de .NET.

Usando la CLI de .NET:

dotnet add package Microsoft.AspNetCore.Mvc.Versioning
dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer

2. Configurar el Versionado de API

En tu archivo Program.cs, configura los servicios de versionado de API.

using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Versioning;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using TodoApiRestfull.Data;
using TodoApiRestfull.Services;
using TodoApiRestfull.Services.Interfaces;

var builder = WebApplication.CreateBuilder(args);
var key = Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Key"] ?? "");

// Configurar Versionado de API
builder.Services.AddApiVersioning(options =>
{
    options.ReportApiVersions = true; // Para enviar versiones de API en los encabezados de respuesta
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion(1, 0); // Versión de API predeterminada cuando no se especifica
    options.ApiVersionReader = ApiVersionReader.Combine(
        new QueryStringApiVersionReader("api-version"),  // Versionado usando cadena de consulta
        new HeaderApiVersionReader("X-Version")          // Versionado usando encabezado de solicitud
    );
});

// Configurar el Explorador de API para versionado
builder.Services.AddVersionedApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = false;
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(key),
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = false,
        ValidateIssuerSigningKey = true,
        LogValidationExceptions = false, 
    };
    options.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = context =>
        {
            context.NoResult();
            context.Response.StatusCode = 401;
            context.Response.ContentType = "text/plain";
            return context.Response.WriteAsync(context.Exception.ToString());
        }
    };
});
//AddControllers es una funcion que permite utilizar controllers para mapear las llamadas
builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
    opt.UseInMemoryDatabase("TodoList"));

builder.Services.AddScoped<ITodoService, TodoService>();
builder.Services.AddEndpointsApiExplorer();
// Configurar Swagger
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "JwtAuthApi", Version = "v1" });
    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer"
    });
    c.AddSecurityRequirement(new OpenApiSecurityRequirement()
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                },
                Scheme = "oauth2",
                Name = "Bearer",
                In = ParameterLocation.Header,
            },
            new List<string>()
        }
    });
});

builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    .AddEnvironmentVariables();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.UseSwagger();
app.UseSwaggerUI();

// Seed the database with the default user
using (var scope = app.Services.CreateScope())
{
    var context = scope.ServiceProvider.GetRequiredService<TodoContext>();
    context.Database.EnsureCreated();
}

app.Run();

3. Definir Versiones de API en Controladores

Anota tus controladores con los atributos de versión de API adecuados.

Ejemplo para un controlador de la versión 1.0:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApiRestfull.Data;
using TodoApiRestfull.Models;
using TodoApiRestfull.Services.Interfaces;

namespace TodoApiRestfull.Controllers
{
    [ApiVersion("1", Deprecated = true)]
    [ApiVersion("2")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [ApiController]
    [Authorize]
    public class TodoItemsController : ControllerBase
    {
        private readonly ITodoService _todoService;

        public TodoItemsController(ITodoService service)
        {
            _todoService = service;
        }

        // GET: api/TodoItems
        [HttpGet]
        public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
        {
            var items = await _todoService.GetTodoItemsAsync();
            return Ok(items);
        }

        // GET: api/TodoItems/5
        [HttpGet("{id}")]
        [MapToApiVersion("1")]
        public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
        {
            var todoItem = await _todoService.GetTodoItemAsync(id);

            if (todoItem == null)
            {
                return NotFound();
            }

            return Ok(todoItem);
        }

        [HttpGet("{id}")]
        [MapToApiVersion("2")]
        public async Task<ActionResult<TodoItem>> GetTodoItemV2(long id)
        {
            return NotFound("error intencional");
        }

        [HttpGet("search/{name}")]
        public async Task<ActionResult<List<TodoItem>>> Search(string name)
        {
            var result = await _todoService.Search(name);

            if(!result.Any()) {
                return NotFound();
            }

            return Ok(result);
        }

        // PUT: api/TodoItems/5
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPut("{id}")]
        public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
        {
            var result = await _todoService.UpdateTodoItemAsync(id, todoItem);

            if (!result)
            {
                return NotFound();
            }

            return NoContent();
        }

        // POST: api/TodoItems
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPost]
        public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
        {
            var createdItem = await _todoService.CreateTodoItemAsync(todoItem);
            return CreatedAtAction("GetTodoItem", new { id = createdItem.Id }, createdItem);
        }

        // DELETE: api/TodoItems/5
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteTodoItem(long id)
        {
            var result = await _todoService.DeleteTodoItemAsync(id);

            if (!result)
            {
                return NotFound();
            }

            return NoContent();
        }
    }
}

Ejemplo para un controlador de la versión 2.0:

using Microsoft.AspNetCore.Mvc;

namespace MyApi.Controllers
{
    [ApiController]
    [Route("api/v{version:apiVersion}/[controller]")]
    [ApiVersion("2.0")]
    public class MyControllerV2 : ControllerBase
    {
        [HttpGet]
        public IActionResult Get() => Ok("Esta es la versión 2.0");
    }
}

4. Probar la API

Ahora puedes probar tu API haciendo solicitudes a diferentes versiones.

  • Versión 1.0: GET /api/v1.0/todoitems/{id}
  • Versión 2.0: GET /api/v2.0/todoitems/{id}

También puedes usar la cadena de consulta o los encabezados si están configurados:

  • Cadena de consulta: GET /api/mycontroller?api-version=1.0
  • Encabezado: GET /api/mycontroller con el encabezado X-Version: 1.0

Consejos Adicionales

  • Integración con Swagger: Si estás usando Swagger, puedes configurarlo para que muestre APIs versionadas usando Swashbuckle.AspNetCore. Esto implica una configuración adicional en Program.cs para generar y documentar Swagger.

  • Deprecación: Puedes marcar una versión de API como obsoleta usando [ApiVersion("1.0", Deprecated = true)].

Siguiendo estos pasos, puedes implementar efectivamente el versionado de API en tu proyecto de .NET Core 8, permitiendo que múltiples versiones de tu API coexistan y se mantengan con el tiempo.

Herramientas para Pruebas:

  1. Postman:

    • Una herramienta versátil para realizar pruebas a servicios RESTful.
    • Permite guardar y organizar colecciones de solicitudes, automatizar pruebas y generar documentación.
  2. SoapUI:

    • Ideal para probar servicios SOAP y REST.
    • Permite crear, ejecutar y automatizar pruebas, así como validar respuestas y simular comportamientos de servicios.
⚠️ **GitHub.com Fallback** ⚠️