API RESTfull con .NET Core

Descargar SDK .NET Core

Ver comandos dotnet

REST es un sistema arquitectónico que tiene como objetivo la construcción de sistemas altamente escalables basándose en 6 conceptos esenciales:

  • Cliente/Servidor
  • Sin estado
  • Cacheable
  • Sistema basado en capas
  • Interfaz unificada
  • Code on demand

Para que una API sea REST debe estar en el nivel 4 de maduración de Richardson. En este estadio se suele denominar RESTful. En este nivel se hace uso de hiperenlaces para indicar URL relacionadas con la actual.

Filosofía API REST

  • Las URLs representan recursos. Hay que usar nombres para los recursos y no verbos:
    • /users/1234 (bien)
    • /getUser/1234 (mal)
  • Los nombres deberían ir en plural:
    • GET /users/1234 (bien)
    • GET /user/1234 (mal)
  • Usar los verbos HTTP correctamente:
    • GET: para obtener datos
    • DELETE: para borrar datos
    • POST/PUT: para añadir y modificar. Cuando se usa cada uno de ellos se determina si la operación es idempotente (usaremos PUT) y si no (POST). Idempotente significa que tras aplicar la misma operación varias veces, el estado no cambia.
  • Simplificar las URL
    • GET /users/1234/books/123?author=seodev
    • GET /books/123?author=seodev (mejor)
  • Usar códigos de respuesta HTTP de manera coherente. Por ejemplo, no devolver un 200 y en payload indicar que hubo un error.
  • Uso de los códigos HTTP
    • 200 (ok): es el más genérico, indica que todo ha ido bien.
    • 201 (created): usar para indicar que se ha creado bien un elemento.
    • 204 (no content): se usa para indicar que todo ha ido bien pero no se quiere enviar respuesta al cliente. Por ejemplo para los borrados.
    • 400 (bad request): usarlo para indicarle al cliente que tiene un error y debe modificar la petición. Devolver un payload con los detalles.
    • 401 (unauthorized): el cliente no está autenticado.
    • 403 (forbidden): el cliente si está autenticado pero no está autorizado para acceder al recurso. También se suele usar un 404 cuando no tengo permisos para acceder al recurso y de esta manera evitar dar información a un posible atacante.
    • 404 (not found): puede indicar que la URL no está bien o que el recurso no se encuentra. Hay polémica en si se debe devolver un 404 cuando no hay recursos o devolver un array vacío.
    • 409 (conflict): se usa para indicar que ocurrió un conflicto de concurrencia al actualizar o intentar realizar alguna operación.
    • 410 (gone): se usa para indicar al cliente que el recurso antes existía pero ya no está disponible.
    • 500 (internal server error)
  • Versionado. La versión se debe indicar siempre de forma explícita para evitar ambigüedades. Evitar añadir un excesivo versionado (ej.: v2.3.4). La versión se puede indicar de varias maneras:
    • GET /v1/users/1234
    • GET /2020-10-03/users/1234
    • GET /users/1234?v=1
    • En el header. Es menos flexible ya que por ejemplo no se podría llamar directamente en el navegador.
  • Devolver respuestas parciales para evitar respuestas pesadas cuando es innecesarios. Ej.:
    GET /users/1234&fields=name,surname

.NET Core funciona como un middleware. Esto es una gran ventaja ya que es muy modularizable.

En la clase Startup se aplican diferentes middlewares que actúan sobre la Request pudiendo modificarla, cortocircuitar la petición, etc… Los middlewares se aplican en el orden que están definidos.

Recomendaciones de Diseño y arquitectura

El controlador de la API no debería hacer prácticamente nada. La lógica de negocio debería estar delegada a servicios o modelos de la capa de dominio. De esta manera se reparten responsabilidades y se desacopla código.

Aún se podría desacoplar más el código y dejar al controlador con prácticamente inútil, excepto para el manejo de validaciones y poco más, usando el patrón comando con MediatoR.

Buenas prácticas

  • Inyectar la connection string en Startup.cs.
  • Separar la configuración de la API Fluent en varias clases de configuración para evitar que crezca demasiado la clase Context de la BD. Registrarlas todas en base al assembly.
  • Patrón Repository: es útil en ocasiones crear una implementación genérica del patrón Repository (clase 10).
  • Usar DTOs en lugar de las Entidades para evitar el overposting. Un usuario que conozca el modelo puede enviar información que no esperamos y crear/actualizar información no permitida.
    Con los DTOs podemos evitar referencias circulares al serializar los datos. Si queremos finalmente optamos por usar las entidades de BD podemos configurar la API para que no intente deserializar las relaciones de las clases. Para ello instalamos el paquete Newtonsoft.json de Mvc y lo configuramos en ConfigureServices la propiedad ReferenceLoopHandling = Ignore.
  • Usar AutoMapper para el mapeo de DTOs <-> Entities. Usar el paquete de Nuget de AutoMapper con inyección de dependencias y configurarlo en ConfigureServices.
  • Usar las validaciones que trae de serie ApiController. En las nuevas versiones de .NET Core ya no es necesario la comprobación de ModelState.IsValid.
    También se puede usar Fluent Validator para validar los DTOs. En lugar de agregar decoradores en los DTOs, es más limpio.
  • Crear una clase para devolver una respuesta homogénea en todo los métodos de la API.
  • Las reglas de negocio deben ir en clases aparte: clases de Servicios. El controlador es el que llama a los servicios y los servicios llaman al repositorio (si existe).
  • Todo lo que sea repetitivo en el controlador se puede mejorar usando un filtro global.
  • Crear excepciones personalizadas para las reglas de negocio. Usar un filtro global para capturarlas.
  • Query parameters opcionales: si hay más de 3 agruparlos en una clase XXQueryFilter.
  • Implementar paginación para volúmenes grandes de datos (clase 13 y 14).
  • Securizar la API con JWT
  • Dividir el código de Startup.cs en clases (clase 20).
  • Si no vamos a usar MVC no añadirlo a Startup.cs. Con AddControllers sería suficiente para una aplicación API.
  • No deberíamos tener ficheros de settings de pre y pro con datos sensibles, por ejemplo cadenas de conexión. Esta información debería estar establecida como variables de entorno en el host correspondiente.
👉  Validar el formato de un número de teléfono en Excel con una función VBA

TIPS

  • Limitar un controlador por rol: [Authorize(Roles= nameof(RoleType.Administrator)]
  • Alternativa a Entity Framework Core para volúmenes de datos grandes: Dapper ORM
  • Si documentamos la API con Swagger y usamos como respuesta de los controladores ActionResult o IActionResult, necesitamos decorar el método del controlador con ProducesPresponseType (clase 12).
  • Despliegue en IIS local y Azure
  • A los parámetros de ruta se le puede indicar el tipo con el que tiene que enrutar con {id:int}. Si se pasa un valor de otro tipo devolverá un 404 en lugar de 400 (Bad Format).
  • Decorando un controlador con [ApiController] añade funcionalidades pensadas para API. Por ejemplo ya no es necesario usar [FromBody] para que interprete que un parámetro viene del body de la petición.
  • Se pueden crear jobs que se inicien con la API creando una clase que herede de IHostedService o de BackgroundService y configurándola en Startup.cs. Esto es útil si cada X tiempo tenemos que hacer alguna tarea como por ejemplo mantener datos en caché.
  • Comprobar excepciones con Assert: Assert.Throws(() => user.setNIF(nif));
  • Validar mapeo de DTOs. No pasa los test si quedan propiedades sin asignar.
  • Crear middleware para controlar las excepciones.

Crear aplicación de tipo Web API

Para crear la API REST con .NET Core utilizaremos la plantilla webapi. Con el comando run de dotnet iniciaremos la apliación.

> dotnet new webapi -n myApi
> cd MyApi
> dotnet run

Tras arrancar la aplicación web, se levanta un servidor web Kestrel en los puertos 5000 (http) y 5001 (https).

Inyectar servicios

Los servicios se configuran mediante inyección de dependencias en el método ConfigureServices de Startup.cs.

services.AddSingleton<IMyService, MyService>();
services.AddTrasient<IMyService, MyService>();
services.AddScoped<IMyService, MyService>();
// con parámetros
services.AddScoped<IMyService, MyService>(sp => new MyService(params..));

Tiempo de vida de los servicios

  • Trasient: se crea una instancia por cada servicio y para cada controlador.
  • Scoped: se crea una instancia del servicio por cada petición.
  • Singleton: el servicio es global solo se instancia una vez.

appsettings.json

En este fichero se colocan las variables de configuración que queremos tener para nuestra API. Por ejemplo, la cadena de conexión a base de datos.

El fichero appsettings.json automáticamente se parsea y está disponible en la interfaz IConfiguration. Se accede a los valores a partir de las claves del JSON. Ejemplo:

Configuration["Logging:LogLevel"]

También se puede parsear una estructura de dicho fichero a una clase en concreto (ej.: LoggingConfig) además se puede inyectar a los controladores. Para ello es necesario el siguiente código en el método ConfigureServices:

services.AddOptions();
services.Configure<LoggingConfig>(Configuration.GetSection("Logging"));

En el controlador se inyectará en el constructor:

public XXXController(IOptions<LoggingConfig> loggingConfig){
    this.logginConfig = loggingConfig.Value;
}

Se puede definir un archivo de settings para diferentes entornos indicándolo en el nombre del fichero appsettings.Environment.json.

Para indicar el entorno que debe ejecutar y así cargar un archivo de configuración u otro, se puede indicar de diferentes maneras (ordenados por prioridad descendente):

  • Mediante el archivo launchSettings.json
  • Por la variable de entorno ASPNETCORE_ENVIRONMENT

El fichero appsettings.json siempre se parsea y si hay definido un entorno en concreto, se leerá el de ese entorno y se hará un merge de los dos ficheros dando prioridad al del entorno actual.

👉  Web.config

También es posible configurar archivos de configuración de otros formatos como XML. Estos tendrán prioridad sobre appsetting.json.

Crear un Middleware

Para crear un middleware simplemente creamos una clase normal que en el constructor recibirá un parámetro de tipo RequestDelegate. Esta es una función que procesa una petición HTTP.

Ejemplo. Un middleware que registra las peticiones con un log.

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

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

        public async Task Invoke(HttpContext httpContext)
        {
            // Código que inspecciona la request
            _logger.LogInformation($"Incoming request: {httpContext.Request.Path}");

            // Llama al siguiente middleware
            await _next(httpContext);

            // Código que inspecciona la response
            _logger.LogInformation($"Outgoing request: {httpContext.Response.StatusCode}")
        }
    }

    // Extensión necesaria para agregar el middleware a IAplicationBuilder
    // En la el método Configure de Startup.cs se llamará con "app.UseLoggingMiddleware();"
    public static class LoggingMiddlewareExtensions
    {
        public static IApplicationBuilder UseLoggingMiddleware(this IApplicationBuilder app)
        {
            return app.UseMiddleware<LoggingMiddleware>();
        }
    }

Autenticación

El mecanismo estándar para autorizar y autenticar con tokens es OAuth con la capa de identidad OIDC (OpenID Connect).

Existen servidores de tokens preparados en base al estándar para emitir tokens como IdentityServer.

El tipo de autenticación por tokens se denomina Bearer.

Configurar Swagger para auto-documentar la API

Instalar paquete de Nuget Swashbuckle.AspNetCore

Habilitar generación de documentación de .NET
Editar el .csproj y añadir:

  <PropertyGroup>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <NoWarn>$(NoWarn);1591</NoWarn>
  </PropertyGroup>

Configurar en Startup.cs
En ConfigureServices añadir:

            services.AddSwaggerGen(doc =>
            {
                doc.SwaggerDoc("v1", new OpenApiInfo { Title = "Task Reports API", Version = "v1" });

                var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
                doc.IncludeXmlComments(xmlPath);
            });

En Configure añadir:

app.UseSwagger();
            app.UseSwaggerUI(options =>
            {
                options.SwaggerEndpoint("/swagger/v1/swagger.json", "Task Reports API V1");
                options.RoutePrefix = string.Empty;
            });

Comentar el código en c# para que lo coja Swagger

Decorar el controlador para que solo muestre en la interfaz de Swagger la información en JSON

 [Produces("application/json")]

Modificar diseño de Swagger

https://www.halldorstefans.com/how-i-customized-the-swagger-ui-in-asp-net-core/

Tests

Tests unitarios

Los test unitarios son aquellos que prueban una unidad de código como un método. Estos deberían testear la lógica de negocio principalmente. Los test unitarios no deberían tener acceso a la BD. En tal caso se deberían crear mocks de la BD. Para ello tenemos el paquete Nuget para hacer Moq.

Crear un proyecto de Tests Unitarios con dotnet usando xunit

> dotnet new xunit --n MyProject.Tests

Lanzar los Tests Unitarios

> dotnet test

También se pueden lanzar de forma gráfica con .NET Core Test Explorer (para VS Code).

Tests de integración

Crear un proyecto de Tests Unitarios

> dotnet new xunit --n MyProject.Integration.Tests

Convertir el proyecto de librería a aplicación web para poder hacer este tipo de tests. Para ello modificar el .csproj:

<Project Sdk="Microsoft.NET.Sdk.Web">

El test de integración testeará una URL de nuestra API. Para realizar esta tarea necesitaremos levantar la API y hacer una Request. Este proceso nos lo simplifica el paquete Microsoft.AspNetCore.Mvc.Testing que instalaremos mediante el comando:

> dotnet add package Microsoft.AspNetCore.Mvc.Testing

Para implementar un test de integración crearemos una clase a la que inyectaremos por constructor una factoría que nos facilitará la llamada de la API

   public class MyControllerTests :
        IClassFixture<WebApplicationFactory<MyProject.Startup>>
    {
        private readonly WebApplicationFactory<MyProject.Startup> _factory;

        public MyControllerTests(WebApplicationFactory<MyProject.Startup> factory)
        {
            _factory = factory;
        }

        [Fact]
        public async Task FindByIdShouldReturnTheUserWithId()
        {
            var client = _factory.CreateClient();
            var response = await client.GetAsync("/xxx/3");
            var json = await response.Content.ReadAsStringAsync();

            var user = JsonConvert.DeserializeObject<User>(json);
            Assert.Equal(3, user.Id);
        }
    }

IClassFixture nos permite compartir el contexto del test entre todas las funciones de la clase. En este caso la creación de la factoría mediante WebApplicationFactory.

Para levantar la API utilizamos la clase Startup original. Lo recomendable sería tener una clase que herede de esta y personalizarla para no incluir ciertos servicios que nos puedan dificultar los tests como la autenticación.

Otro punto importante es que en un escenario real la api se levantaría apuntando a una BD específica para test donde tengo información acorde para los tests.

👇Tu comentario