Una guía increíble sobre cómo construir API RESTful con ASP.NET Core

Una guía paso a paso sobre cómo implementar API RESTful limpias y mantenibles

Foto de Jefferson Santos, publicada en Unsplash

Visión de conjunto

RESTful no es un término nuevo. Se refiere a un estilo arquitectónico donde los servicios web reciben y envían datos desde y hacia las aplicaciones del cliente. El objetivo de estas aplicaciones es centralizar los datos que utilizarán las diferentes aplicaciones cliente.

Elegir las herramientas adecuadas para escribir servicios RESTful es crucial, ya que debemos preocuparnos por la escalabilidad, el mantenimiento, la documentación y todos los demás aspectos relevantes. ASP.NET Core nos brinda una API potente y fácil de usar que es excelente para lograr estos objetivos.

En este artículo, le mostraré cómo escribir una API RESTful bien estructurada para un escenario "casi" del mundo real, utilizando el marco ASP.NET Core. Voy a detallar patrones y estrategias comunes para simplificar el proceso de desarrollo.

También le mostraré cómo integrar marcos y bibliotecas comunes, como Entity Framework Core y AutoMapper, para ofrecer las funcionalidades necesarias.

Prerrequisitos

Espero que tenga conocimiento de los conceptos de programación orientada a objetos.

Aunque voy a cubrir muchos detalles del lenguaje de programación C #, le recomiendo que tenga un conocimiento básico de este tema.

También supongo que sabe qué es REST, cómo funciona el protocolo HTTP, qué son los puntos finales API y qué es JSON. Aquí hay un gran tutorial introductorio sobre este tema. El requisito final es que comprenda cómo funcionan las bases de datos relacionales.

Para codificar conmigo, deberá instalar .NET Core 2.2, así como Postman, la herramienta que voy a usar para probar la API. Le recomiendo que use un editor de código como Visual Studio Code para desarrollar la API. Elija el editor de código que prefiera. Si elige este editor de código, le recomiendo que instale la extensión C # para tener un mejor resaltado de código.

Puede encontrar un enlace al repositorio de Github de la API al final de este artículo, para verificar el resultado final.

El alcance

Vamos a escribir una API web ficticia para un supermercado. Imaginemos que tenemos que implementar el siguiente alcance:

  • Cree un servicio RESTful que permita a las aplicaciones del cliente administrar el catálogo de productos del supermercado. Necesita exponer puntos finales para crear, leer, editar y eliminar categorías de productos, como productos lácteos y cosméticos, y también para administrar productos de estas categorías.
  • Para las categorías, necesitamos almacenar sus nombres. Para los productos, necesitamos almacenar sus nombres, unidad de medida (por ejemplo, KG para productos medidos por peso), cantidad en el paquete (por ejemplo, 10 si el producto es un paquete de galletas) y sus respectivas categorías.

Para simplificar el ejemplo, no manejaré productos en stock, envío de productos, seguridad y ninguna otra funcionalidad. El alcance dado es suficiente para mostrarle cómo funciona ASP.NET Core.

Para desarrollar este servicio, básicamente necesitamos dos puntos finales de API: uno para administrar categorías y otro para administrar productos. En términos de comunicación JSON, podemos pensar en las respuestas de la siguiente manera:

Punto final de API: / api / categories

Respuesta JSON (para solicitudes GET):

{
  [
    {"id": 1, "nombre": "Frutas y verduras"},
    {"id": 2, "nombre": "Panes"},
    … // Otras categorías
  ]
}

Punto final de API: / api / products

Respuesta JSON (para solicitudes GET):

{
  [
    {
      "id": 1,
      "nombre": "Azúcar",
      "amountInPackage": 1,
      "unitOfMeasurement": "KG"
      "categoría": {
        "id": 3,
        "nombre": "Azúcar"
      }
    },
    … // Otros productos
  ]
}

Comencemos a escribir la solicitud.

Paso 1 - Creando la API

En primer lugar, tenemos que crear la estructura de carpetas para el servicio web, y luego tenemos que usar las herramientas .NET CLI para andamiar una API web básica. Abra el terminal o el símbolo del sistema (depende del sistema operativo que esté utilizando) y escriba los siguientes comandos, en secuencia:

mkdir src / Supermarket.API
cd src / Supermarket.API
dotnet new webapi

Los primeros dos comandos simplemente crean un nuevo directorio para la API y cambian la ubicación actual a la nueva carpeta. El último genera un nuevo proyecto siguiendo la plantilla API web, que es el tipo de aplicación que estamos desarrollando. Puede leer más sobre estos comandos y otras plantillas de proyectos que puede generar al consultar este enlace.

El nuevo directorio ahora tendrá la siguiente estructura:

Estructura del proyecto

Resumen de estructura

Una aplicación ASP.NET Core consta de un grupo de middlewares (pequeñas partes de la aplicación adjuntas a la canalización de la aplicación, que manejan solicitudes y respuestas) configuradas en la clase de Inicio. Si ya ha trabajado con marcos como Express.js anteriormente, este concepto no es nuevo para usted.

Cuando se inicia la aplicación, se llama al método Main, desde la clase Program. Crea un servidor web predeterminado utilizando la configuración de inicio, exponiendo la aplicación a través de HTTP a través de un puerto específico (de forma predeterminada, el puerto 5000 para HTTP y 5001 para HTTPS).

Eche un vistazo a la clase ValuesController dentro de la carpeta Controllers. Expone los métodos a los que se llamará cuando la API reciba solicitudes a través de la ruta / api / valores.

No se preocupe si no comprende alguna parte de este código. Voy a detallar cada uno cuando desarrolle los puntos finales API necesarios. Por ahora, simplemente elimine esta clase, ya que no la vamos a usar.

Paso 2: creación de los modelos de dominio

Voy a aplicar algunos conceptos de diseño que harán que la aplicación sea simple y fácil de mantener.

Escribir un código que usted mismo pueda comprender y mantener no es tan difícil, pero debe tener en cuenta que trabajará como parte de un equipo. Si no te preocupas por cómo escribes tu código, el resultado será un monstruo que te causará dolores de cabeza constantes a ti y a tus compañeros de equipo. Suena extremo, ¿verdad? Pero créeme, esa es la verdad.

wtf: la medición de la calidad del código por smitty42 está licenciada bajo CC-BY-ND 2.0

Comencemos escribiendo la capa de dominio. Esta capa tendrá nuestras clases de modelos, las clases que representarán nuestros productos y categorías, así como repositorios e interfaces de servicios. Explicaré estos dos últimos conceptos en un momento.

Dentro del directorio Supermarket.API, cree una nueva carpeta llamada Dominio. Dentro de la nueva carpeta de dominio, cree otra llamada Modelos. El primer modelo que tenemos que agregar a esta carpeta es la Categoría. Inicialmente, será una clase simple de Objeto CLR simple antiguo (POCO). Significa que la clase solo tendrá propiedades para describir su información básica.

La clase tiene una propiedad Id, para identificar la categoría, y una propiedad Name. También tenemos una propiedad de Productos. Este último será utilizado por Entity Framework Core, el ORM que la mayoría de las aplicaciones ASP.NET Core usan para conservar los datos en una base de datos, para mapear la relación entre categorías y productos. También tiene sentido pensar en términos de programación orientada a objetos, ya que una categoría tiene muchos productos relacionados.

También tenemos que crear el modelo del producto. En la misma carpeta, agregue una nueva clase de Producto.

El producto también tiene propiedades para el Id y el nombre. También es una propiedad CantidadInPaquete, que indica cuántas unidades del producto tenemos en un paquete (recuerde el ejemplo de las galletas del alcance de la aplicación) y una propiedad UnitOfMeasurement. Este está representado por un tipo de enumeración, que representa una enumeración de posibles unidades de medida. El ORM utilizará las dos últimas propiedades, CategoryId y Category, para mapear la relación entre productos y categorías. Indica que un producto tiene una, y solo una, categoría.

Definamos la última parte de nuestros modelos de dominio, la enumeración EUnitOfMeasurement.

Por convención, las enumeraciones no necesitan comenzar con una "E" delante de sus nombres, pero en algunas bibliotecas y marcos encontrará este prefijo como una forma de distinguir enumeraciones de interfaces y clases.

El código es realmente sencillo. Aquí definimos solo un puñado de posibilidades para unidades de medida, sin embargo, en un sistema de supermercado real, puede tener muchas otras unidades de medida, y tal vez un modelo separado para eso.

Observe el atributo Descripción aplicado sobre cada posibilidad de enumeración. Un atributo es una forma de definir metadatos sobre clases, interfaces, propiedades y otros componentes del lenguaje C #. En este caso, lo utilizaremos para simplificar las respuestas del punto final de la API de productos, pero no tiene que preocuparse por eso por ahora. Volveremos aquí más tarde.

Nuestros modelos básicos están listos para ser utilizados. Ahora podemos comenzar a escribir el punto final de la API que administrará todas las categorías.

Paso 3 - La API de Categorías

En la carpeta Controllers, agregue una nueva clase llamada CategoriesController.

Por convención, todas las clases en esta carpeta que terminan con el sufijo "Controlador" se convertirán en controladores de nuestra aplicación. Significa que van a manejar solicitudes y respuestas. Debe heredar esta clase de la clase Controlador, definida en el espacio de nombres Microsoft.AspNetCore.Mvc.

Un espacio de nombres consta de un grupo de clases, interfaces, enumeraciones y estructuras relacionadas. Puede pensarlo como algo similar a los módulos del lenguaje Javascript, o paquetes de Java.

El nuevo controlador debe responder a través de la ruta / api / categorías. Logramos esto agregando el atributo Ruta sobre el nombre de la clase, especificando un marcador de posición que indica que la ruta debe usar el nombre de la clase sin el sufijo del controlador, por convención.

Comencemos a gestionar las solicitudes GET. En primer lugar, cuando alguien solicita datos de / api / categories a través del verbo GET, la API debe devolver todas las categorías. Podemos crear un servicio de categoría para este propósito.

Conceptualmente, un servicio es básicamente una clase o interfaz que define métodos para manejar cierta lógica empresarial. Es una práctica común en muchos lenguajes de programación diferentes crear servicios para manejar la lógica de negocios, como autenticación y autorización, pagos, flujos de datos complejos, almacenamiento en caché y tareas que requieren cierta interacción entre otros servicios o modelos.

Usando servicios, podemos aislar el manejo de solicitudes y respuestas de la lógica real necesaria para completar las tareas.

El servicio que vamos a crear inicialmente definirá un solo comportamiento o método: un método de listado. Esperamos que este método devuelva todas las categorías existentes en la base de datos.

Para simplificar, en este caso no trataremos la paginación o el filtrado de datos. Escribiré un artículo en el futuro que muestre cómo manejar fácilmente estas funciones.

Para definir un comportamiento esperado para algo en C # (y en otros lenguajes orientados a objetos, como Java, por ejemplo), definimos una interfaz. Una interfaz indica cómo debería funcionar algo, pero no implementa la lógica real del comportamiento. La lógica se implementa en clases que implementan la interfaz. Si este concepto no es claro para usted, no se preocupe. Lo entenderás en un momento.

Dentro de la carpeta Dominio, cree un nuevo directorio llamado Servicios. Allí, agregue una interfaz llamada ICategoryService. Por convención, todas las interfaces deben comenzar con la letra mayúscula "I" en C #. Defina el código de interfaz de la siguiente manera:

Las implementaciones del método ListAsync deben devolver asincrónicamente una enumeración de categorías.

La clase Task, que encapsula el retorno, indica asincronía. Necesitamos pensar en un método asincrónico debido al hecho de que tenemos que esperar a que la base de datos complete alguna operación para devolver los datos, y este proceso puede llevar un tiempo. Observe también el sufijo "asíncrono". Es una convención que indica que nuestro método debe ejecutarse de forma asincrónica.

Tenemos muchas convenciones, ¿verdad? Personalmente me gusta, porque mantiene las aplicaciones fáciles de leer, incluso si eres nuevo en una empresa que usa tecnología .NET.

“- Ok, definimos esta interfaz, pero no hace nada. ¿Cómo puede ser útil?

Si vienes de un idioma como Javascript u otro lenguaje no fuertemente tipado, este concepto puede parecer extraño.

Las interfaces nos permiten abstraer el comportamiento deseado de la implementación real. Usando un mecanismo conocido como inyección de dependencia, podemos implementar estas interfaces y aislarlas de otros componentes.

Básicamente, cuando usa la inyección de dependencia, define algunos comportamientos usando una interfaz. Luego, crea una clase que implementa la interfaz. Finalmente, vincula las referencias de la interfaz a la clase que creó.

"- Suena muy confuso. ¿No podemos simplemente crear una clase que haga estas cosas por nosotros? "

Continuemos implementando nuestra API y comprenderá por qué usar este enfoque.

Cambie el código de CategoriesController de la siguiente manera:

He definido una función constructora para nuestro controlador (se llama a un constructor cuando se crea una nueva instancia de una clase) y recibe una instancia de ICategoryService. Significa que la instancia puede ser cualquier cosa que implemente la interfaz de servicio. Almaceno esta instancia en un campo privado de solo lectura _categoryService. Utilizaremos este campo para acceder a los métodos de implementación de nuestro servicio de categoría.

Por cierto, el prefijo de subrayado es otra convención común para denotar un campo. Esta convención, en especial, no es recomendada por la directriz oficial de convención de nomenclatura de .NET, pero es una práctica muy común como una forma de evitar tener que usar la palabra clave "this" para distinguir los campos de clase de las variables locales. Personalmente, creo que es mucho más limpio de leer, y muchos marcos y bibliotecas usan esta convención.

Debajo del constructor, definí el método que manejará las solicitudes de / api / categories. El atributo HttpGet le dice a la canalización de ASP.NET Core que lo use para manejar solicitudes GET (este atributo se puede omitir, pero es mejor escribirlo para facilitar la legibilidad).

El método utiliza nuestra instancia de servicio de categoría para enumerar todas las categorías y luego devuelve las categorías al cliente. La canalización de framework maneja la serialización de datos a un objeto JSON. El tipo IEnumerable le dice al marco que queremos devolver una enumeración de categorías, y el tipo Task, precedido por la palabra clave asincrónica, le dice a la tubería que este método debe ejecutarse de forma asincrónica. Finalmente, cuando definimos un método asíncrono, tenemos que usar la palabra clave wait para tareas que pueden llevar un tiempo.

Ok, definimos la estructura inicial de nuestra API. Ahora, es necesario implementar realmente el servicio de categorías.

Paso 4: Implementación del servicio de categorías

En la carpeta raíz de la API (la carpeta Supermarket.API), cree una nueva llamada Servicios. Aquí pondremos todas las implementaciones de servicios. Dentro de la nueva carpeta, agregue una nueva clase llamada CategoryService. Cambie el código de la siguiente manera:

Es simplemente el código básico para la implementación de la interfaz, pero aún no manejamos ninguna lógica. Pensemos en cómo debería funcionar el método de listado.

Necesitamos acceder a la base de datos y devolver todas las categorías, luego debemos devolver estos datos al cliente.

Una clase de servicio no es una clase que deba manejar el acceso a datos. Hay un patrón llamado Patrón de repositorio que se usa para administrar datos de bases de datos.

Cuando se utiliza el patrón de repositorio, definimos clases de repositorio, que básicamente encapsulan toda la lógica para manejar el acceso a datos. Estos repositorios exponen métodos para enumerar, crear, editar y eliminar objetos de un modelo determinado, de la misma manera que puede manipular colecciones. Internamente, estos métodos se comunican con la base de datos para realizar operaciones CRUD, aislando el acceso a la base de datos del resto de la aplicación.

Nuestro servicio necesita hablar con un repositorio de categorías para obtener la lista de objetos.

Conceptualmente, un servicio puede "hablar" con uno o más repositorios u otros servicios para realizar operaciones.

Puede parecer redundante crear una nueva definición para manejar la lógica de acceso a datos, pero verá que aislar esta lógica de la clase de servicio es muy ventajoso.

Creemos un repositorio que se encargará de intermediar la comunicación de la base de datos como una forma de persistir en las categorías.

Paso 5 - El repositorio de categorías y la capa de persistencia

Dentro de la carpeta Dominio, cree un nuevo directorio llamado Repositorios. Luego, agregue una nueva interfaz llamada ICategoryRespository. Defina la interfaz de la siguiente manera:

El código inicial es básicamente idéntico al código de la interfaz de servicio.

Una vez definida la interfaz, podemos volver a la clase de servicio y terminar de implementar el método de listado, utilizando una instancia de ICategoryRepository para devolver los datos.

Ahora tenemos que implementar la lógica real del repositorio de categorías. Antes de hacerlo, tenemos que pensar cómo vamos a acceder a la base de datos.

Por cierto, ¡todavía no tenemos una base de datos!

Utilizaremos Entity Framework Core (lo llamaré EF Core por simplicidad) como nuestra base de datos ORM. Este marco viene con ASP.NET Core como ORM predeterminado y expone una API amigable que nos permite asignar clases de nuestras aplicaciones a tablas de bases de datos.

EF Core también nos permite diseñar nuestra aplicación primero y luego generar una base de datos de acuerdo con lo que definimos en nuestro código. Esta técnica se llama código primero. Usaremos el primer método de código para generar una base de datos (en este ejemplo, de hecho, voy a usar una base de datos en memoria, pero podrá cambiarla fácilmente a una instancia de servidor SQL Server o MySQL, por ejemplo).

En la carpeta raíz de la API, cree un nuevo directorio llamado Persistencia. Este directorio tendrá todo lo que necesitamos para acceder a la base de datos, como implementaciones de repositorios.

Dentro de la nueva carpeta, cree un nuevo directorio llamado Contextos y luego agregue una nueva clase llamada AppDbContext. Esta clase debe heredar DbContext, una clase que EF Core usa para asignar sus modelos a las tablas de la base de datos. Cambie el código de la siguiente manera:

El constructor que agregamos a esta clase es responsable de pasar la configuración de la base de datos a la clase base a través de la inyección de dependencia. Verás en un momento cómo funciona esto.

Ahora, tenemos que crear dos propiedades DbSet. Estas propiedades son conjuntos (colecciones de objetos únicos) que asignan modelos a tablas de bases de datos.

Además, tenemos que asignar las propiedades de los modelos a las columnas de la tabla respectiva, especificando qué propiedades son claves primarias, cuáles son claves foráneas, los tipos de columna, etc. Podemos hacer esto anulando el método OnModelCreating, utilizando una función llamada API fluida para especifique el mapeo de la base de datos. Cambie la clase AppDbContext de la siguiente manera:

El código es intuitivo.

Especificamos a qué tablas deben mapearse nuestros modelos. Además, establecemos las claves principales, utilizando el método HasKey, las columnas de la tabla, el método Property y algunas restricciones, como IsRequired, HasMaxLength y ValueGeneratedOnAdd, todo con expresiones lambda de forma "fluida" (métodos de encadenamiento).

Eche un vistazo al siguiente código:

builder.Entity  ()
       .Tiene muchos (p => p.Productos)
       .WithOne (p => p.Category)
       .HasForeignKey (p => p.CategoryId);

Aquí estamos especificando una relación entre tablas. Decimos que una categoría tiene muchos productos, y establecemos las propiedades que correlacionarán esta relación (Productos, de la categoría Categoría y Categoría, de la clase Producto). También establecemos la clave foránea (Id. De categoría).

Eche un vistazo a este tutorial si desea aprender cómo configurar relaciones uno a uno y muchos a muchos usando EF Core, así como cómo usarlo como un todo.

También hay una configuración para sembrar datos, a través del método HasData:

builder.Entity  () .HasData
(
  nueva Categoría {Id = 100, Nombre = "Frutas y Verduras"},
  nueva Categoría {Id = 101, Name = "Dairy"}
);

Aquí simplemente agregamos dos categorías de ejemplo por defecto. Eso es necesario para probar nuestro punto final de API después de que lo terminemos.

Aviso: estamos configurando manualmente las propiedades de Id aquí porque el proveedor en memoria requiere que funcione. Estoy configurando los identificadores a números grandes para evitar la colisión entre los identificadores generados automáticamente y los datos semilla.
Esta limitación no existe en los proveedores de bases de datos relacionales verdaderas, por lo que si desea utilizar una base de datos como SQL Server, por ejemplo, no tiene que especificar estos identificadores. Verifique este problema de Github si desea comprender este comportamiento.

Una vez implementada la clase de contexto de la base de datos, podemos implementar el repositorio de categorías. Agregue una nueva carpeta llamada Repositorios dentro de la carpeta Persistence y luego agregue una nueva clase llamada BaseRepository.

Esta clase es solo una clase abstracta que heredarán todos nuestros repositorios. Una clase abstracta es una clase que no tiene instancias directas. Tienes que crear clases directas para crear las instancias.

El BaseRepository recibe una instancia de nuestro AppDbContext a través de inyección de dependencia y expone una propiedad protegida (una propiedad a la que solo pueden acceder las clases secundarias) llamada _context, que da acceso a todos los métodos que necesitamos para manejar las operaciones de la base de datos.

Agregue una nueva clase en la misma carpeta llamada CategoryRepository. Ahora realmente implementaremos la lógica del repositorio:

El repositorio hereda el BaseRepository e implementa ICategoryRepository.

Observe lo simple que es implementar el método de listado. Usamos el conjunto de bases de datos de Categorías para acceder a la tabla de categorías y luego llamamos al método de extensión ToListAsync, que es responsable de transformar el resultado de una consulta en una colección de categorías.

EF Core traduce nuestra llamada al método en una consulta SQL, de la manera más eficiente posible. La consulta solo se ejecuta cuando llama a un método que transformará sus datos en una colección, o cuando usa un método para tomar datos específicos.

Ahora tenemos una implementación limpia del controlador de categorías, el servicio y el repositorio.

Hemos separado las preocupaciones, creando clases que solo hacen lo que se supone que deben hacer.

El último paso antes de probar la aplicación es vincular nuestras interfaces a las clases respectivas utilizando el mecanismo de inyección de dependencia de ASP.NET Core.

Paso 6 - Configuración de la inyección de dependencia

Es hora de que finalmente entiendas cómo funciona este concepto.

En la carpeta raíz de la aplicación, abra la clase de Inicio. Esta clase es responsable de configurar todo tipo de configuraciones cuando se inicia la aplicación.

Los métodos ConfigureServices y Configure son llamados en tiempo de ejecución por el pipeline de framework para configurar cómo debería funcionar la aplicación y qué componentes debe usar.

Eche un vistazo al método ConfigureServices. Aquí solo tenemos una línea, que configura la aplicación para usar la canalización MVC, lo que básicamente significa que la aplicación manejará las solicitudes y respuestas utilizando clases de controlador (hay más cosas sucediendo aquí detrás de escena, pero eso es lo que necesita saber) por ahora).

Podemos usar el método ConfigureServices, accediendo al parámetro de servicios, para configurar nuestros enlaces de dependencia. Limpie el código de clase eliminando todos los comentarios y cambie el código de la siguiente manera:

Mira este fragmento de código:

services.AddDbContext  (opciones => {
  options.UseInMemoryDatabase ("supermercado-api-en-memoria");
});

Aquí configuramos el contexto de la base de datos. Le decimos a ASP.NET Core que use nuestro AppDbContext con una implementación de base de datos en memoria, que se identifica mediante la cadena que se pasa como argumento a nuestro método. Por lo general, el proveedor en memoria se usa cuando escribimos pruebas de integración, pero lo estoy usando aquí por simplicidad. De esta manera, no necesitamos conectarnos a una base de datos real para probar la aplicación.

La configuración de estas líneas configura internamente el contexto de nuestra base de datos para la inyección de dependencias utilizando una vida útil con alcance.

La duración del alcance le dice a la canalización de ASP.NET Core que cada vez que necesita resolver una clase que recibe una instancia de AppDbContext como argumento de constructor, debe usar la misma instancia de la clase. Si no hay una instancia en la memoria, la canalización creará una nueva instancia y la reutilizará en todas las clases que la necesiten, durante una solicitud determinada. De esta manera, no necesita crear manualmente la instancia de clase cuando necesita usarla.

Hay otros ámbitos de por vida que puede consultar leyendo la documentación oficial.

La técnica de inyección de dependencia nos brinda muchas ventajas, tales como:

  • Código de reutilización;
  • Mejor productividad, ya que cuando tenemos que cambiar la implementación, no necesitamos molestarnos en cambiar cien lugares donde usas esa función;
  • Puede probar fácilmente la aplicación ya que podemos aislar lo que tenemos que probar usando simulacros (implementación falsa de clases) donde tenemos que pasar interfaces como argumentos de constructor;
  • Cuando una clase necesita recibir más dependencias a través de un constructor, no tiene que cambiar manualmente todos los lugares donde se crean las instancias (¡eso es increíble!).

Después de configurar el contexto de la base de datos, también vinculamos nuestro servicio y repositorio a las clases respectivas.

services.AddScoped  ();
services.AddScoped  ();

Aquí también usamos una vida útil con alcance porque estas clases tienen que usar internamente la clase de contexto de la base de datos. Tiene sentido especificar el mismo alcance en este caso.

Ahora que configuramos nuestros enlaces de dependencia, tenemos que hacer un pequeño cambio en la clase Programa, para que la base de datos pueda sembrar correctamente nuestros datos iniciales. Este paso solo es necesario cuando se utiliza el proveedor de base de datos en memoria (consulte este problema de Github para comprender por qué).

Era necesario cambiar el método Principal para garantizar que nuestra base de datos se "creará" cuando se inicie la aplicación, ya que estamos utilizando un proveedor en memoria. Sin este cambio, las categorías que queremos sembrar no se crearán.

Con todas las funciones básicas implementadas, es hora de probar nuestro punto final API.

Paso 7: prueba de la API de categorías

Abra el terminal o el símbolo del sistema en la carpeta raíz API y escriba el siguiente comando:

dotnet run

El comando anterior inicia la aplicación. La consola mostrará una salida similar a esta:

info: Microsoft.EntityFrameworkCore.Infrastructure [10403]
Entity Framework Core 2.2.0-rtm-35687 inicializó "AppDbContext" utilizando el proveedor "Microsoft.EntityFrameworkCore.InMemory" con opciones: StoreName = supermarket-api-in-memory
info: Microsoft.EntityFrameworkCore.Update [30100]
Se guardaron 2 entidades en la tienda en memoria.
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager [0]
El perfil del usuario está disponible. Usando "C: \ Users \ evgomes \ AppData \ Local \ ASP.NET \ DataProtection-Keys" como depósito de claves y Windows DPAPI para cifrar las claves en reposo.
Entorno de alojamiento: desarrollo
Ruta raíz del contenido: C: \ Users \ evgomes \ Desktop \ Tutorials \ src \ Supermarket.API
Ahora escuchando en: https: // localhost: 5001
Ahora escuchando en: http: // localhost: 5000
Solicitud iniciada. Presione Ctrl + C para apagar.

Puede ver que se llamó a EF Core para inicializar la base de datos. Las últimas líneas muestran en qué puertos se está ejecutando la aplicación.

Abra un navegador y navegue a http: // localhost: 5000 / api / categories (o a la URL que se muestra en la salida de la consola). Si ve un error de seguridad debido a HTTPS, simplemente agregue una excepción para la aplicación.

El navegador mostrará los siguientes datos JSON como salida:

[
  {
     "id": 100,
     "nombre": "Frutas y Verduras",
     "productos": []
  },
  {
     "id": 101,
     "name": "Dairy",
     "productos": []
  }
]

Aquí vemos los datos que agregamos a la base de datos cuando configuramos el contexto de la base de datos. Este resultado confirma que nuestro código está funcionando.

Creó un punto final GET API con muy pocas líneas de código, y tiene una estructura de código que es realmente fácil de cambiar debido a la arquitectura de la API.

Ahora, es hora de mostrarle lo fácil que es cambiar este código cuando tiene que ajustarlo debido a las necesidades comerciales.

Paso 8 - Crear un recurso de categoría

Si recuerda la especificación del punto final de la API, habrá notado que nuestra respuesta JSON real tiene una propiedad adicional: una variedad de productos. Eche un vistazo al ejemplo de la respuesta deseada:

{
  [
    {"id": 1, "nombre": "Frutas y verduras"},
    {"id": 2, "nombre": "Panes"},
    … // Otras categorías
  ]
}

La matriz de productos está presente en nuestra respuesta JSON actual porque nuestro modelo de Categoría tiene una propiedad de Productos, que EF Core necesita para correlacionar los productos de una categoría determinada.

No queremos esta propiedad en nuestra respuesta, pero no podemos cambiar nuestra clase de modelo para excluir esta propiedad. Causaría que EF Core arrojara errores cuando tratamos de administrar datos de categorías, y también rompería el diseño de nuestro modelo de dominio porque no tiene sentido tener una categoría de producto que no tiene productos.

Para devolver datos JSON que contienen solo los identificadores y los nombres de las categorías de supermercados, tenemos que crear una clase de recurso.

Una clase de recurso es una clase que contiene solo información básica que se intercambiará entre las aplicaciones cliente y los puntos finales API, generalmente en forma de datos JSON, para representar cierta información en particular.

Todas las respuestas de los puntos finales API deben devolver un recurso.

Es una mala práctica devolver la representación del modelo real como respuesta, ya que puede contener información que la aplicación cliente no necesita o que no tiene permiso (por ejemplo, un modelo de usuario podría devolver información de la contraseña del usuario). , lo que sería un gran problema de seguridad).

Necesitamos un recurso para representar solo nuestras categorías, sin los productos.

Ahora que sabe qué es un recurso, impleméntelo. En primer lugar, detenga la aplicación en ejecución presionando Ctrl + C en la línea de comando. En la carpeta raíz de la aplicación, cree una nueva carpeta llamada Recursos. Allí, agregue una nueva clase llamada CategoryResource.

Tenemos que asignar nuestra colección de modelos de categoría, que es proporcionada por nuestro servicio de categoría, a una colección de recursos de categoría.

Utilizaremos una biblioteca llamada AutoMapper para manejar la asignación entre objetos. AutoMapper es una biblioteca muy popular en el mundo .NET, y se usa en muchos proyectos comerciales y de código abierto.

Escriba las siguientes líneas en la línea de comando para agregar AutoMapper a nuestra aplicación:

dotnet agregar paquete AutoMapper
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

Para usar AutoMapper, tenemos que hacer dos cosas:

  • Regístralo para la inyección de dependencia;
  • Cree una clase que le diga a AutoMapper cómo manejar el mapeo de clases.

En primer lugar, abra la clase de inicio. En el método ConfigureServices, después de la última línea, agregue el siguiente código:

servicios.AddAutoMapper ();

Esta línea maneja todas las configuraciones necesarias de AutoMapper, como registrarlo para inyección de dependencia y escanear la aplicación durante el inicio para configurar perfiles de mapeo.

Ahora, en el directorio raíz, agregue una nueva carpeta llamada Mapping, luego agregue una clase llamada ModelToResourceProfile. Cambia el código de esta manera:

La clase hereda Profile, un tipo de clase que AutoMapper usa para verificar cómo funcionarán nuestras asignaciones. En el constructor, creamos un mapa entre la clase de modelo Category y la clase CategoryResource. Dado que las propiedades de las clases tienen los mismos nombres y tipos, no tenemos que usar ninguna configuración especial para ellas.

El paso final consiste en cambiar el controlador de categorías para usar AutoMapper para manejar nuestra asignación de objetos.

Cambié el constructor para recibir una instancia de implementación IMapper. Puede usar estos métodos de interfaz para usar los métodos de asignación de AutoMapper.

También cambié el método GetAllAsync para asignar nuestra enumeración de categorías a una enumeración de recursos utilizando el método Map. Este método recibe una instancia de la clase o colección que queremos asignar y, a través de definiciones de tipo genérico, define a qué tipo de clase o colección debe asignarse.

Tenga en cuenta que cambiamos fácilmente la implementación sin tener que adaptar la clase de servicio o el repositorio, simplemente inyectando una nueva dependencia (IMapper) al constructor.

La inyección de dependencia hace que su aplicación sea fácil de cambiar y mantener, ya que no tiene que romper toda la implementación de su código para agregar o quitar funciones.

Probablemente se dio cuenta de que no solo la clase de controlador, sino todas las clases que reciben dependencias (incluidas las propias dependencias) se resolvieron automáticamente para recibir las clases correctas de acuerdo con las configuraciones de enlace.

La inyección de dependencia es increíble, ¿no?

Ahora, inicie la API nuevamente usando el comando de ejecución dotnet y diríjase a http: // localhost: 5000 / api / categories para ver la nueva respuesta JSON.

Estos son los datos de respuesta que deberías ver

Ya tenemos nuestro punto final GET. Ahora, creemos un nuevo punto final para POST (crear) categorías.

Paso 9 - Crear nuevas categorías

Cuando tratamos con la creación de recursos, debemos preocuparnos por muchas cosas, tales como:

  • Validación de datos e integridad de datos;
  • Autorización para crear recursos;
  • Manejo de errores;
  • Inicio sesión.

No mostraré cómo lidiar con la autenticación y la autorización en este tutorial, pero puede ver cómo implementar fácilmente estas funciones leyendo mi tutorial sobre la autenticación de tokens web JSON.

Además, existe un marco muy popular llamado ASP.NET Identity que proporciona soluciones integradas con respecto a la seguridad y el registro de usuarios que puede usar en sus aplicaciones. Incluye proveedores para trabajar con EF Core, como un IdentityDbContext incorporado que puede usar. Puedes aprender más acerca de esto aquí.

Escribamos un punto final HTTP POST que cubra los otros escenarios (excepto el registro, que puede cambiar de acuerdo con diferentes ámbitos y herramientas).

Antes de crear el nuevo punto final, necesitamos un nuevo recurso. Este recurso asignará los datos que las aplicaciones cliente envían a este punto final (en este caso, el nombre de la categoría) a una clase de nuestra aplicación.

Como estamos creando una nueva categoría, todavía no tenemos una identificación, y eso significa que necesitamos un recurso que represente una categoría que contenga solo su nombre.

En la carpeta Recursos, agregue una nueva clase llamada SaveCategoryResource:

Observe los atributos Required y MaxLength aplicados sobre la propiedad Name. Estos atributos se denominan anotaciones de datos. La canalización de ASP.NET Core utiliza estos metadatos para validar solicitudes y respuestas. Como sugieren los nombres, el nombre de la categoría es obligatorio y tiene una longitud máxima de 30 caracteres.

Ahora definamos la forma del nuevo punto final API. Agregue el siguiente código al controlador de categorías:

Le decimos al framework que este es un punto final HTTP POST que usa el atributo HttpPost.

Observe el tipo de respuesta de este método, Tarea . Los métodos presentes en las clases de controlador se denominan acciones, y tienen esta firma porque podemos devolver más de un resultado posible después de que la aplicación ejecute la acción.

En este caso, si el nombre de la categoría no es válido, o si algo sale mal, debemos devolver una respuesta de código 400 (solicitud incorrecta), que generalmente contiene un mensaje de error que las aplicaciones del cliente pueden usar para tratar el problema, o podemos tener un 200 respuestas (éxito) con datos si todo sale bien.

Hay muchos tipos de acciones que puede usar como respuesta, pero en general, podemos usar esta interfaz y ASP.NET Core usará una clase predeterminada para eso.

El atributo FromBody le dice a ASP.NET Core que analice los datos del cuerpo de la solicitud en nuestra nueva clase de recursos. Significa que cuando se envía un JSON que contiene el nombre de la categoría a nuestra aplicación, el marco lo analizará automáticamente a nuestra nueva clase.

Ahora, implementemos nuestra lógica de ruta. Tenemos que seguir algunos pasos para crear con éxito una nueva categoría:

  • Primero, tenemos que validar la solicitud entrante. Si la solicitud no es válida, debemos devolver una respuesta de solicitud incorrecta que contenga los mensajes de error;
  • Luego, si la solicitud es válida, tenemos que asignar nuestro nuevo recurso a nuestra clase de modelo de categoría usando AutoMapper;
  • Ahora necesitamos llamar a nuestro servicio, diciéndole que guarde nuestra nueva categoría. Si la lógica de guardado se ejecuta sin problemas, debería devolver una respuesta que contenga los datos de nuestra nueva categoría. Si no, debería darnos una indicación de que el proceso falló y un posible mensaje de error;
  • Finalmente, si hay un error, devolvemos una solicitud incorrecta. Si no, asignamos nuestro nuevo modelo de categoría a un recurso de categoría y devolvemos una respuesta exitosa al cliente, que contiene los datos de la nueva categoría.

Parece ser complicado, pero es muy fácil implementar esta lógica usando la arquitectura de servicio que estructuramos para nuestra API.

Comencemos validando la solicitud entrante.

Paso 10: validación del cuerpo de la solicitud con el estado del modelo

Los controladores ASP.NET Core tienen una propiedad llamada ModelState. Esta propiedad se llena durante la ejecución de la solicitud antes de llegar a la ejecución de nuestra acción. Es una instancia de ModelStateDictionary, una clase que contiene información como si la solicitud es válida y posibles mensajes de error de validación.

Cambie el código de punto final de la siguiente manera:

El código verifica si el estado del modelo (en este caso, los datos enviados en el cuerpo de la solicitud) no es válido, verificando nuestras anotaciones de datos. Si no es así, la API devuelve una solicitud incorrecta (con un código de estado 400) y los mensajes de error predeterminados que proporcionan nuestros metadatos de anotaciones.

El método ModelState.GetErrorMessages () aún no está implementado. Es un método de extensión (un método que amplía la funcionalidad de una clase o interfaz ya existente) que voy a implementar para convertir los errores de validación en cadenas simples para volver al cliente.

Agregue una nueva carpeta Extensiones en la raíz de nuestra API y luego agregue una nueva clase ModelStateExtensions.

Todos los métodos de extensión deben ser estáticos, así como las clases donde se declaran. Significa que no manejan datos de instancia específicos y que se cargan solo una vez cuando se inicia la aplicación.

La palabra clave this delante de la declaración del parámetro le dice al compilador de C # que la trate como un método de extensión. El resultado es que podemos llamarlo como un método normal de esta clase, ya que incluimos la directiva de uso respectiva donde queremos usar la extensión.

La extensión usa consultas LINQ, una característica muy útil de .NET que nos permite consultar y transformar datos usando expresiones encadenables. Las expresiones aquí transforman los métodos de error de validación en una lista de cadenas que contienen los mensajes de error.

Importe el espacio de nombres Supermarket.API.Extensions en el controlador de categorías antes de pasar al siguiente paso.

usando Supermarket.API.Extensions;

Continuemos implementando nuestra lógica de punto final asignando nuestro nuevo recurso a una clase de modelo de categoría.

Paso 11: asignación del nuevo recurso

Ya hemos definido un perfil de mapeo para transformar modelos en recursos. Ahora necesitamos un nuevo perfil que haga lo contrario.

Agregue una nueva clase ResourceToModelProfile en la carpeta Mapping:

Nada nuevo aquí. Gracias a la magia de la inyección de dependencias, AutoMapper registrará automáticamente este perfil cuando se inicie la aplicación, y no tenemos que cambiar ningún otro lugar para usarlo.

Ahora podemos asignar nuestro nuevo recurso a la clase de modelo respectiva:

Paso 12: aplicar el patrón de solicitud-respuesta para manejar la lógica de guardado

Ahora tenemos que implementar la lógica más interesante: guardar una nueva categoría. Esperamos que nuestro servicio lo haga.

La lógica de guardado puede fallar debido a problemas al conectarse a la base de datos, o tal vez porque cualquier regla comercial interna invalida nuestros datos.

Si algo sale mal, no podemos simplemente lanzar un error, porque podría detener la API, y la aplicación cliente no sabría cómo manejar el problema. Además, potencialmente tendríamos algún mecanismo de registro que registraría el error.

El contrato del método de ahorro, es decir, la firma del método y el tipo de respuesta, debe indicarnos si el proceso se ejecutó correctamente. Si el proceso va bien, recibiremos los datos de la categoría. Si no, tenemos que recibir, al menos, un mensaje de error que indique por qué falló el proceso.

Podemos implementar esta característica aplicando el patrón de solicitud-respuesta. Este patrón de diseño empresarial encapsula nuestros parámetros de solicitud y respuesta en clases como una forma de encapsular información que nuestros servicios usarán para procesar alguna tarea y devolver información a la clase que está utilizando el servicio.

Este patrón nos brinda algunas ventajas, tales como:

  • Si necesitamos cambiar nuestro servicio para recibir más parámetros, no tenemos que romper su firma;
  • Podemos definir un contrato estándar para nuestra solicitud y / o respuestas;
  • Podemos manejar la lógica de negocios y los posibles errores sin detener el proceso de solicitud, y no necesitaremos usar toneladas de bloques try-catch.

Creemos un tipo de respuesta estándar para nuestros métodos de servicios que manejan cambios de datos. Para cada solicitud de este tipo, queremos saber si la solicitud se ejecuta sin problemas. Si falla, queremos devolver un mensaje de error al cliente.

En la carpeta Dominio, dentro de Servicios, agregue un nuevo directorio llamado Comunicación. Agregue una nueva clase allí llamada BaseResponse.

Esa es una clase abstracta que heredarán nuestros tipos de respuesta.

La abstracción define una propiedad Success, que indicará si las solicitudes se completaron correctamente, y una propiedad Message, que tendrá el mensaje de error si algo falla.

Tenga en cuenta que estas propiedades son necesarias y que solo las clases heredadas pueden establecer estos datos porque las clases secundarias deben pasar esta información a través de la función de constructor.

Consejo: no es una buena práctica definir clases base para todo, porque las clases base acoplan su código y le impiden modificarlo fácilmente. Prefiero usar la composición sobre la herencia.
Para el alcance de esta API, no es realmente un problema usar clases base, ya que nuestros servicios no crecerán mucho. Si se da cuenta de que un servicio o aplicación crecerá y cambiará con frecuencia, evite usar una clase base.

Ahora, en la misma carpeta, agregue una nueva clase llamada SaveCategoryResponse.

El tipo de respuesta también establece una propiedad Categoría, que contendrá los datos de nuestra categoría si la solicitud finaliza correctamente.

Tenga en cuenta que he definido tres constructores diferentes para esta clase:

  • Uno privado, que va a pasar los parámetros de éxito y mensaje a la clase base, y también establece la propiedad Categoría;
  • Un constructor que recibe solo la categoría como parámetro. Éste creará una respuesta exitosa, llamando al constructor privado para establecer las propiedades respectivas;
  • Un tercer constructor que solo especifica el mensaje. Este se usará para crear una respuesta de falla.

Debido a que C # admite múltiples constructores, simplificamos la creación de respuestas sin definir un método diferente para manejar esto, simplemente usando diferentes constructores.

Ahora podemos cambiar nuestra interfaz de servicio para agregar el nuevo contrato de método de guardado.

Cambie la interfaz ICategoryService de la siguiente manera:

Simplemente pasaremos una categoría a este método y manejará toda la lógica necesaria para guardar los datos del modelo, orquestando repositorios y otros servicios necesarios para hacerlo.

Tenga en cuenta que no estoy creando una clase de solicitud específica aquí, ya que no necesitamos ningún otro parámetro para realizar esta tarea. Hay un concepto en la programación de computadoras llamado KISS, abreviatura de Keep it Simple, Stupid. Básicamente, dice que debe mantener su aplicación lo más simple posible.

Recuerde esto al diseñar sus aplicaciones: aplique solo lo que necesita para resolver un problema. No sobre-ingenie su aplicación.

Ahora podemos terminar nuestra lógica de punto final:

Después de validar los datos de la solicitud y asignar el recurso a nuestro modelo, lo pasamos a nuestro servicio para conservar los datos.

Si algo falla, la API devuelve una solicitud incorrecta. De lo contrario, la API asigna la nueva categoría (que ahora incluye datos como el nuevo Id) a nuestro CategoryResource creado previamente y lo envía al cliente.

Ahora implementemos la lógica real del servicio.

Paso 13 - La lógica de la base de datos y el patrón de la unidad de trabajo

Como vamos a conservar los datos en la base de datos, necesitamos un nuevo método en nuestro repositorio.

Agregue un nuevo método AddAsync a la interfaz ICategoryRepository:

Ahora, implementemos este método en nuestra clase de repositorio real:

Aquí simplemente estamos agregando una nueva categoría a nuestro conjunto.

Cuando agregamos una clase a un DBSet <>, EF Core comienza a rastrear todos los cambios que ocurren en nuestro modelo y utiliza estos datos en el estado actual para generar consultas que insertarán, actualizarán o eliminarán modelos.

La implementación actual simplemente agrega el modelo a nuestro conjunto, pero nuestros datos aún no se guardarán.

Hay un método llamado SaveChanges presente en la clase de contexto al que tenemos que llamar para ejecutar realmente las consultas en la base de datos. No lo llamé aquí porque un repositorio no debería conservar datos, es solo una colección de objetos en memoria.

Este tema es muy controvertido incluso entre desarrolladores experimentados de .NET, pero déjenme explicarles por qué no deberían llamar a SaveChanges en las clases de repositorio.

Podemos pensar conceptualmente en un repositorio como cualquier otra colección presente en .NET Framework. Cuando se trata de una colección en .NET (y muchos otros lenguajes de programación, como Javascript y Java), generalmente puede:

  • Agregue nuevos elementos (como cuando inserta datos en listas, matrices y diccionarios);
  • Encuentra o filtra elementos;
  • Eliminar un elemento de la colección;
  • Reemplazar un elemento determinado o actualizarlo.

Piensa en una lista del mundo real. Imagina que estás escribiendo una lista de compras para comprar cosas en un supermercado (¿qué coincidencia, no?).

En la lista, escribe todas las frutas que necesita comprar. Puede agregar frutas a esta lista, eliminar una fruta si deja de comprarla, o puede reemplazar el nombre de una fruta. Pero no puedes guardar frutas en la lista. No tiene sentido decir algo así en inglés simple.

Consejo: cuando diseñe clases e interfaces en lenguajes de programación orientados a objetos, intente usar lenguaje natural para verificar si lo que está haciendo parece ser correcto.
Tiene sentido, por ejemplo, decir que un hombre implementa una interfaz de persona, pero no tiene sentido decir que un hombre implementa una cuenta.

Si desea "guardar" las listas de frutas (en este caso, para comprar todas las frutas), pague y el supermercado procesa los datos de inventario para verificar si tienen que comprar más frutas de un proveedor o no.

La misma lógica se puede aplicar al programar. Los repositorios no deben guardar, actualizar o eliminar datos. En cambio, deberían delegarlo a una clase diferente para manejar esta lógica.

Hay otro problema al guardar datos directamente en un repositorio: no puede usar transacciones.

Imagine que nuestra aplicación tiene un mecanismo de registro que almacena algún nombre de usuario y la acción se realiza cada vez que se realiza un cambio en los datos de la API.

Ahora imagine que, por alguna razón, tiene una llamada a un servicio que actualiza el nombre de usuario (no es un escenario común, pero consideremoslo).

Usted acepta que para cambiar el nombre de usuario en una tabla de usuarios ficticia, primero debe actualizar todos los registros para saber correctamente quién realizó esa operación, ¿verdad?

Ahora imagine que hemos implementado el método de actualización para usuarios y registros en diferentes repositorios, y ambos llaman SaveChanges. ¿Qué sucede si uno de estos métodos falla en la mitad del proceso de actualización? Terminarás con inconsistencia de datos.

Deberíamos guardar nuestros cambios en la base de datos solo después de que todo termine. Para hacer esto, tenemos que usar una transacción, que es básicamente una característica que la mayoría de las bases de datos implementan para guardar datos solo después de que finaliza una operación compleja.

"- Ok, entonces si no podemos guardar cosas aquí, ¿dónde deberíamos hacerlo?"

Un patrón común para manejar este problema es el Patrón de Unidad de Trabajo. Este patrón consiste en una clase que recibe nuestra instancia de AppDbContext como una dependencia y expone métodos para iniciar, completar o cancelar transacciones.

Utilizaremos una implementación simple de una unidad de trabajo para abordar nuestro problema aquí.

Agregue una nueva interfaz dentro de la carpeta Repositorios de la capa de dominio llamada IUnitOfWork:

Como puede ver, solo expone un método que completará asincrónicamente las operaciones de administración de datos.

Agreguemos la implementación real ahora.

Agregue una nueva clase llamada UnitOfWork en la carpeta RepositoriesRepositories de la capa Persistence:

Esa es una implementación simple y limpia que solo guardará todos los cambios en la base de datos después de que termine de modificarla utilizando sus repositorios.

Si investiga implementaciones del patrón de Unidad de trabajo, encontrará otras más complejas que implementan operaciones de reversión.

Dado que EF Core ya implementa el patrón de repositorio y la unidad de trabajo detrás de escena, no tenemos que preocuparnos por un método de reversión.

" - ¿Qué? Entonces, ¿por qué tenemos que crear todas estas interfaces y clases?

La separación de la lógica de persistencia de las reglas comerciales ofrece muchas ventajas en términos de reutilización y mantenimiento del código. Si usamos EF Core directamente, terminaremos teniendo clases más complejas que no serán tan fáciles de cambiar.

Imagine que en el futuro decide cambiar el marco ORM a uno diferente, como Dapper, por ejemplo, o si tiene que implementar consultas SQL simples debido al rendimiento. Si combina la lógica de sus consultas con sus servicios, será difícil cambiar la lógica, ya que tendrá que hacerlo en muchas clases.

Usando el patrón de repositorio, simplemente puede implementar una nueva clase de repositorio y vincularlo mediante inyección de dependencia.

Entonces, básicamente, si usa EF Core directamente en sus servicios y tiene que cambiar algo, eso es lo que obtendrá:

Como dije, EF Core implementa los patrones de la Unidad de Trabajo y Repositorio detrás de escena. Podemos considerar nuestras propiedades DbSet <> como repositorios. Además, SaveChanges solo conserva datos en caso de éxito para todas las operaciones de la base de datos.

Ahora que sabe qué es una unidad de trabajo y por qué usarla con repositorios, implementemos la lógica del servicio real.

Gracias a nuestra arquitectura desacoplada, simplemente podemos pasar una instancia de UnitOfWork como dependencia para esta clase.

Nuestra lógica de negocios es bastante simple.

Primero, intentamos agregar la nueva categoría a la base de datos y luego la API intenta guardarla, envolviendo todo dentro de un bloque try-catch.

Si algo falla, la API llama a un servicio de registro ficticio y devuelve una respuesta que indica un error.

Si el proceso finaliza sin problemas, la aplicación devuelve una respuesta exitosa, enviando los datos de nuestra categoría. Simple, verdad?

Consejo: en las aplicaciones del mundo real, no debe envolver todo dentro de un bloque genérico try-catch, sino que debe manejar todos los posibles errores por separado.
Simplemente agregar un bloque try-catch no cubrirá la mayoría de los posibles escenarios fallidos. Asegúrese de corregir el manejo de errores del implemento.

El último paso antes de probar nuestra API es vincular la interfaz de la unidad de trabajo a su clase respectiva.

Agregue esta nueva línea al método ConfigureServices de la clase Startup:

services.AddScoped  ();

¡Ahora probémoslo!

Paso 14: prueba de nuestro punto final POST con Postman

Inicie nuestra aplicación nuevamente usando dotnet run.

No podemos usar el navegador para probar un punto final POST. Usemos Postman para probar nuestros puntos finales. Es una herramienta muy útil para probar las API RESTful.

Abra Postman y cierre los mensajes de introducción. Verás una pantalla como esta:

Pantalla que muestra opciones para probar puntos finales

Cambie GET seleccionado por defecto en el cuadro de selección a POST.

Escriba la dirección API en el campo Ingresar URL de solicitud.

Tenemos que proporcionar los datos del cuerpo de la solicitud para enviar a nuestra API. Haga clic en el elemento del menú Cuerpo, luego cambie la opción que se muestra debajo a sin formato.

Cartero mostrará una opción de Texto a la derecha. Cámbielo a JSON (aplicación / json) y pegue los siguientes datos JSON a continuación:

{
  "nombre": ""
}
Pantalla justo antes de enviar una solicitud

Como puede ver, vamos a enviar una cadena de nombre vacía a nuestro nuevo punto final.

Haz clic en el botón Enviar. Recibirá una salida como esta:

Como puede ver, nuestra lógica de validación funciona.

¿Recuerdas la lógica de validación que creamos para el punto final? ¡Esta salida es la prueba de que funciona!

Observe también el código de estado 400 que se muestra a la derecha. El resultado de BadRequest agrega automáticamente este código de estado a la respuesta.

Ahora cambiemos los datos JSON a uno válido para ver la nueva respuesta:

Finalmente, el resultado que esperábamos tener

La API creó correctamente nuestro nuevo recurso.

Hasta ahora, nuestra API puede enumerar y crear categorías. Aprendiste muchas cosas sobre el lenguaje C #, el framework ASP.NET Core y también enfoques de diseño comunes para estructurar tus API.

Continuemos con nuestra API de categorías creando el punto final para actualizar las categorías.

De ahora en adelante, ya que le expliqué la mayoría de los conceptos, aceleraré las explicaciones y me enfocaré en nuevos temas para no perder su tiempo. ¡Vamonos!

Paso 15 - Actualizar categorías

Para actualizar categorías, necesitamos un punto final HTTP PUT.

La lógica que tenemos que codificar es muy similar a la POST:

  • Primero, tenemos que validar la solicitud entrante usando ModelState;
  • Si la solicitud es válida, la API debe asignar el recurso entrante a una clase de modelo utilizando AutoMapper;
  • Luego, debemos llamar a nuestro servicio, diciéndole que actualice la categoría, proporcionando el Id. De categoría respectivo y los datos actualizados;
  • Si no hay una categoría con el ID dado en la base de datos, devolveremos una solicitud incorrecta. Podríamos usar un resultado NotFound en su lugar, pero realmente no importa para este alcance, ya que proporcionamos un mensaje de error a las aplicaciones del cliente;
  • Si la lógica de guardado se ejecuta correctamente, el servicio debe devolver una respuesta que contenga los datos actualizados de la categoría. De lo contrario, debería darnos una indicación de que el proceso falló y un mensaje que indique por qué;
  • Finalmente, si hay un error, la API devuelve una solicitud incorrecta. De lo contrario, asigna el modelo de categoría actualizado a un recurso de categoría y devuelve una respuesta exitosa a la aplicación cliente.

Agreguemos el nuevo método PutAsync a la clase de controlador:

Si lo compara con la lógica POST, notará que aquí solo tenemos una diferencia: el atributo HttPut especifica un parámetro que debe recibir la ruta dada.

Llamaremos a este punto final especificando el Id. De categoría como el último fragmento de URL, como / api / categories / 1. La canalización de ASP.NET Core analiza este fragmento con el parámetro del mismo nombre.

Ahora tenemos que definir la firma del método UpdateAsync en la interfaz ICategoryService:

Ahora pasemos a la lógica real.

Paso 16 - La lógica de actualización

Para actualizar nuestra categoría, primero, debemos devolver los datos actuales de la base de datos, si existe. También necesitamos actualizarlo en nuestro DBSet <>.

Agreguemos dos nuevos contratos de métodos a nuestra interfaz ICategoryService:

Hemos definido el método FindByIdAsync, que devolverá asincrónicamente una categoría de la base de datos, y el método Actualizar. Tenga en cuenta que el método de actualización no es asíncrono ya que la API EF Core no requiere un método asíncrono para actualizar los modelos.

Ahora implementemos la lógica real en la clase CategoryRepository:

Finalmente podemos codificar la lógica del servicio:

La API intenta obtener la categoría de la base de datos. Si el resultado es nulo, devolvemos una respuesta que indica que la categoría no existe. Si la categoría existe, necesitamos establecer su nuevo nombre.

La API, entonces, intenta guardar los cambios, como cuando creamos una nueva categoría. Si el proceso se completa, el servicio devuelve una respuesta exitosa. De lo contrario, se ejecuta la lógica de registro y el punto final recibe una respuesta que contiene un mensaje de error.

Ahora probémoslo. Primero, agreguemos una nueva categoría para tener una identificación válida para usar. Podríamos usar los identificadores de las categorías que iniciamos en nuestra base de datos, pero quiero hacerlo de esta manera para mostrarle que nuestra API actualizará el recurso correcto.

Ejecute la aplicación nuevamente y, utilizando Postman, PUBLICAR una nueva categoría en la base de datos:

Agregar una nueva categoría para actualizarla más tarde

Con un ID válido en las manos, cambie la opción POST a PUT en el cuadro de selección y agregue el valor de ID al final de la URL. Cambie la propiedad del nombre a un nombre diferente y envíe la solicitud para verificar el resultado:

Los datos de la categoría se actualizaron correctamente.

Puede enviar una solicitud GET al punto final de la API para asegurarse de que editó correctamente el nombre de la categoría:

Ese es el resultado de una solicitud GET ahora

La última operación que tenemos que implementar para categorías es la exclusión de categorías. Hagámoslo creando un punto final Eliminar HTTP.

Paso 17 - Eliminar categorías

La lógica para eliminar categorías es realmente fácil de implementar, ya que la mayoría de los métodos que necesitamos se crearon previamente.

Estos son los pasos necesarios para que nuestra ruta funcione:

  • La API necesita llamar a nuestro servicio, diciéndole que elimine nuestra categoría, proporcionando la identificación correspondiente;
  • Si no hay una categoría con la ID dada en la base de datos, el servicio debe devolver un mensaje que lo indique;
  • Si la lógica de eliminación se ejecuta sin problemas, el servicio debería devolver una respuesta que contenga los datos de nuestra categoría eliminada. Si no, debería darnos una indicación de que el proceso falló y un posible mensaje de error;
  • Finalmente, si hay un error, la API devuelve una solicitud incorrecta. De lo contrario, la API asigna la categoría actualizada a un recurso y devuelve una respuesta exitosa al cliente.

Comencemos agregando la nueva lógica de punto final:

El atributo HttpDelete también define una plantilla de identificación.

Antes de agregar la firma DeleteAsync a nuestra interfaz ICategoryService, necesitamos hacer una pequeña refactorización.

El nuevo método de servicio debe devolver una respuesta que contenga los datos de la categoría, de la misma manera que lo hicimos para los métodos PostAsync y UpdateAsync. Podríamos reutilizar SaveCategoryResponse para este propósito, pero en este caso no estamos guardando datos.

Para evitar crear una nueva clase con la misma forma para cumplir con este requisito, simplemente podemos cambiar el nombre de SaveCategoryResponse a CategoryResponse.

Si está usando Visual Studio Code, puede abrir la clase SaveCategoryResponse, colocar el cursor del mouse sobre el nombre de la clase y usar la opción Cambiar todas las ocurrencias para cambiar el nombre de la clase:

Manera fácil de cambiar el nombre en todos los archivos

Asegúrese de cambiar el nombre del nombre del archivo también.

Agreguemos la firma del método DeleteAsync a la interfaz ICategoryService:

Antes de implementar la lógica de eliminación, necesitamos un nuevo método en nuestro repositorio.

Agregue la firma del método Remove a la interfaz ICategoryRepository:

anular Eliminar (categoría de categoría);

Y ahora agregue la implementación real en la clase de repositorio:

EF Core requiere que la instancia de nuestro modelo se pase al método Remove para comprender correctamente qué modelo estamos eliminando, en lugar de simplemente pasar un Id.

Finalmente, implementemos la lógica en la clase CategoryService:

No hay nada nuevo aquí. El servicio intenta encontrar la categoría por ID y luego llama a nuestro repositorio para eliminar la categoría. Finalmente, la unidad de trabajo completa la transacción ejecutando la operación real en la base de datos.

"- Hola, pero ¿qué hay de los productos de cada categoría? ¿No necesita crear un repositorio y eliminar los productos primero para evitar errores? "

La respuesta es no. Gracias al mecanismo de seguimiento de EF Core, cuando cargamos un modelo desde la base de datos, el marco sabe qué relaciones tiene el modelo. Si lo eliminamos, EF Core sabe que debería eliminar todos los modelos relacionados primero, de forma recursiva.

Podemos deshabilitar esta función al asignar nuestras clases a las tablas de la base de datos, pero está fuera del alcance de este tutorial. Echa un vistazo aquí si quieres aprender sobre esta función.

Ahora es el momento de probar nuestro nuevo punto final. Ejecute la aplicación nuevamente y envíe una solicitud DELETE usando Postman de la siguiente manera:

Como puede ver, la API eliminó la categoría existente sin problemas

Podemos verificar que nuestra API funcione correctamente enviando una solicitud GET:

Ahora recibimos solo una categoría como resultado

Hemos terminado las categorías API. Ahora es el momento de pasar a la API de productos.

Paso 18 - La API de productos

Hasta ahora has aprendido cómo implementar todos los verbos HTTP básicos para manejar operaciones CRUD con ASP.NET Core. Pasemos al siguiente nivel implementando nuestra API de productos.

No detallaré todos los verbos HTTP nuevamente porque sería exhaustivo. Para la parte final de este tutorial, cubriré solo la solicitud GET, para mostrarle cómo incluir entidades relacionadas al consultar datos de la base de datos y cómo usar los atributos de Descripción que definimos para los valores de enumeración EUnitOfMeasurement.

Agregue un nuevo controlador a la carpeta Controladores llamado ProductsController.

Antes de codificar algo aquí, tenemos que crear el recurso del producto.

Permítanme actualizar su memoria mostrando nuevamente cómo debería verse nuestro recurso:

{
 [
  {
   "id": 1,
   "nombre": "Azúcar",
   "amountInPackage": 1,
   "unitOfMeasurement": "KG"
   "categoría": {
   "id": 3,
   "nombre": "Azúcar"
   }
  },
  … // Otros productos
 ]
}

Queremos una matriz JSON que contenga todos los productos de la base de datos.

Los datos JSON difieren del modelo del producto por dos cosas:

  • La unidad de medida se muestra de forma más corta, solo muestra su abreviatura;
  • Generamos los datos de categoría sin incluir la propiedad CategoryId.

Para representar la unidad de medida, podemos usar una propiedad de cadena simple en lugar de un tipo de enumeración (por cierto, no tenemos un tipo de enumeración predeterminado para los datos JSON, por lo que tenemos que transformarlo en un tipo diferente).

Ahora que ahora sabemos cómo dar forma al nuevo recurso, vamos a crearlo. Agregue una nueva clase ProductResource en la carpeta Recursos:

Ahora tenemos que configurar la asignación entre la clase de modelo y nuestra nueva clase de recurso.

La configuración de asignación será casi la misma que la utilizada para otras asignaciones, pero aquí tenemos que manejar la transformación de nuestra enumeración EUnitOfMeasurement en una cadena.

¿Recuerdas el atributo StringValue aplicado sobre los tipos de enumeración? Ahora le mostraré cómo extraer esta información utilizando una potente función del marco .NET: la API Reflection.

Reflection API es un poderoso conjunto de recursos que nos permite extraer y manipular metadatos. Muchos marcos y bibliotecas (incluido ASP.NET Core) hacen uso de estos recursos para manejar muchas cosas detrás de escena.

Ahora veamos cómo funciona en la práctica. Agregue una nueva clase a la carpeta Extensiones llamada EnumExtensions.

Puede parecer aterrador la primera vez que mira el código, pero no es tan complejo. Analicemos la definición del código para comprender cómo funciona.

Primero, definimos un método genérico (un método que puede recibir más de un tipo de argumento, en este caso, representado por la declaración TEnum) que recibe una enumeración dada como argumento.

Como enum es una palabra clave reservada en C #, agregamos una @ delante del nombre del parámetro para que sea un nombre válido.

El primer paso de ejecución de este método es obtener la información de tipo (la definición de clase, interfaz, enumeración o estructura) del parámetro utilizando el método GetType.

Luego, el método obtiene el valor de enumeración específico (por ejemplo, Kilogramo) usando GetField (@ enum.ToString ()).

La siguiente línea encuentra todos los atributos de Descripción aplicados sobre el valor de enumeración y almacena sus datos en una matriz (en algunos casos, podemos especificar múltiples atributos para una misma propiedad).

La última línea usa una sintaxis más corta para verificar si tenemos al menos un atributo de descripción para el tipo de enumeración. Si es así, devolvemos el valor de Descripción proporcionado por este atributo. Si no, devolvemos la enumeración como una cadena, utilizando la conversión predeterminada.

Los ?. operator (un operador condicional nulo) verifica si el valor es nulo antes de acceder a su propiedad.

Los ?? El operador (un operador de fusión nula) le dice a la aplicación que devuelva el valor a la izquierda si no está vacío, o el valor a la derecha de lo contrario.

Ahora que tenemos un método de extensión para extraer descripciones, configuremos nuestra asignación entre modelo y recurso. Gracias a AutoMapper, podemos hacerlo con solo una línea adicional.

Abra la clase ModelToResourceProfile y cambie el código de esta manera:

Esta sintaxis le dice a AutoMapper que use el nuevo método de extensión para convertir nuestro valor EUnitOfMeasurement en una cadena que contenga su descripción. Simple, verdad? Puede leer la documentación oficial para comprender la sintaxis completa.

Tenga en cuenta que no hemos definido ninguna configuración de asignación para la propiedad de categoría. Como configuramos previamente la asignación para categorías y porque el modelo del producto tiene una propiedad de categoría del mismo tipo y nombre, AutoMapper sabe implícitamente que debe asignarla usando la configuración respectiva.

Ahora agreguemos el código de punto final. Cambie el código de ProductsController:

Básicamente, la misma estructura definida para el controlador de categorías.

Vayamos a la parte de servicio. Agregue una nueva interfaz IProductService en la carpeta Servicios presente en la capa Dominio:

Debería haberse dado cuenta de que necesitamos un repositorio antes de implementar realmente el nuevo servicio.

Agregue una nueva interfaz llamada IProductRepository en la carpeta respectiva:

Ahora implementemos el repositorio. Tenemos que implementarlo casi de la misma manera que lo hicimos para el repositorio de categorías, excepto que necesitamos devolver los datos de categoría respectivos de cada producto al consultar los datos.

EF Core, por defecto, no incluye entidades relacionadas con sus modelos cuando consulta datos porque podría ser muy lento (imagine un modelo con diez entidades relacionadas, todas las entidades relacionadas tienen sus propias relaciones).

Para incluir los datos de categorías, solo necesitamos una línea adicional:

Observe la llamada a Incluir (p => p.Category). Podemos encadenar esta sintaxis para incluir tantas entidades como sea necesario al consultar datos. EF Core lo traducirá a una unión cuando realice la selección.

Ahora podemos implementar la clase ProductService de la misma manera que lo hicimos para las categorías:

Atemos las nuevas dependencias cambiando la clase de inicio:

Finalmente, antes de probar la API, cambiemos la clase AppDbContext para incluir algunos productos al inicializar la aplicación para que podamos ver los resultados:

Agregué dos productos ficticios asociándolos a las categorías que iniciamos al inicializar la aplicación.

¡Hora de probar! Ejecute la API nuevamente y envíe una solicitud GET a / api / products usando Postman:

Voilà! Aquí están nuestros productos.

¡Y eso es! ¡Felicidades!

Ahora tiene una base sobre cómo construir una API RESTful usando ASP.NET Core usando una arquitectura desacoplada. Aprendió muchas cosas del marco .NET Core, cómo trabajar con C #, los conceptos básicos de EF Core y AutoMapper y muchos patrones útiles para usar al diseñar sus aplicaciones.

Puede verificar la implementación completa de la API, que contiene los otros verbos HTTP para productos, verificando el repositorio de Github:

Conclusión

ASP.NET Core es un gran marco para usar al crear aplicaciones web. Viene con muchas API útiles que puede usar para crear aplicaciones limpias y fáciles de mantener. Considérelo como una opción al crear aplicaciones profesionales.

Este artículo no ha cubierto todos los aspectos de una API profesional, pero aprendió todos los conceptos básicos. También aprendiste muchos patrones útiles para resolver patrones que enfrentamos a diario.

Espero que hayas disfrutado este artículo y espero que te haya sido útil. Agradezco sus comentarios para comprender cómo puedo mejorar esto.

Referencias para seguir aprendiendo

Tutoriales de .NET Core - Microsoft Docs

Documentación de ASP.NET Core - Microsoft Docs