Dotnet Architecture Microservices
Dotnet Architecture Microservices
Dotnet Architecture Microservices
Para que sea más fácil empezar a trabajar, la guía se centra en una aplicación de
referencia en contenedor y basada en microservicios que puede explorar. La aplicación
de referencia está disponible en el repositorio de GitHub eShopOnContainers .
Vínculos de acción
Este libro electrónico también está disponible en formato PDF (solo versión en
inglés) Descargar
Introducción
Las empresas cada vez ahorran más costos, resuelven más problemas de
implementación y mejoran más las operaciones de DevOps y producción mediante el
uso de contenedores. Microsoft ha lanzado recientemente innovaciones en los
contenedores de Windows y Linux con la creación de productos como Azure Kubernetes
Service y Azure Service Fabric, contando además con la colaboración de líderes del
sector como Docker, Mesosphere y Kubernetes. Estos productos ofrecen soluciones de
contenedores que ayudan a las empresas a compilar e implementar aplicaciones a
velocidad y escala de nube, sea cual sea la plataforma o las herramientas que hayan
elegido.
Una vez que haya estudiado esta guía, el siguiente paso que debería dar es obtener
información sobre los microservicios listos para la producción en Microsoft Azure.
Versión
Esta guía se ha revisado para tratar la versión .NET 6 junto con muchas actualizaciones
adicionales relacionadas con la misma "oleada" de tecnologías (es decir, Azure y otras
tecnologías de terceros) que coincidan en el tiempo con la versión de .NET 6. Este es el
motivo por el que la versión del libro se ha actualizado también a la versión 6.0.
Créditos
Coautores:
Editores:
Mike Pope
Steve Hoag
Participantes y revisores:
Copyright
PUBLICADO POR
Este libro se proporciona “tal cual” y expresa las opiniones del autor. Las opiniones y la
información expresados en este libro, incluidas las direcciones URL y otras referencias a
sitios web de Internet, pueden cambiar sin previo aviso.
El logotipo de la ballena de Docker es una marca registrada de Docker, Inc. Se usa con
permiso.
Siguiente
Introducción a Containers y Docker
Artículo • 29/03/2023
Sugerencia
Descargar PDF
Del mismo modo que los contenedores de mercancías permiten su transporte por
barco, tren o camión independientemente de la carga de su interior, los contenedores
de software actúan como una unidad estándar de implementación de software que
puede contener diferentes dependencias y código. De esta manera, la inclusión del
software en contenedor permite a los desarrolladores y los profesionales de TI
implementarlo en entornos con pocas modificaciones o ninguna en absoluto.
Cada contenedor puede ejecutar una aplicación web o un servicio al completo, como se
muestra en la figura 2-1. En este ejemplo, el host de Docker es un host de contenedor, y
App 1, App 2, Svc 1 y Svc 2 son aplicaciones o servicios en contenedor.
Anterior Siguiente
¿Qué es Docker?
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Figura 2-2. Docker implementa contenedores en todas las capas de la nube híbrida.
Los contenedores de Docker se pueden ejecutar en cualquier lugar, a nivel local en el
centro de datos de cliente, en un proveedor de servicios externo o en la nube, en Azure.
Los contenedores de imagen de Docker se pueden ejecutar de forma nativa en Linux y
Windows. Sin embargo, las imágenes de Windows solo pueden ejecutarse en hosts de
Windows y las imágenes de Linux pueden ejecutarse en hosts de Linux y hosts de
Windows (con una máquina virtual Linux de Hyper-V, hasta el momento), donde host
significa un servidor o una máquina virtual.
Las máquinas virtuales incluyen la Los contenedores incluyen la aplicación y todas sus
aplicación, las bibliotecas o los dependencias. Sin embargo, comparten el kernel del sistema
archivos binarios necesarios y un operativo con otros contenedores, que se ejecutan como
sistema operativo invitado procesos aislados en el espacio de usuario en el sistema
completo. La virtualización operativo host. (Excepto en los contenedores de Hyper-V, en
completa requiere más recursos que cada contenedor se ejecuta dentro de una máquina
que la inclusión en contenedores. virtual especial por contenedor).
Figura 2-3. Comparación de las máquinas virtuales tradicionales con los contenedores
de Docker
Para las máquinas virtuales, hay tres niveles de base en el servidor host, de manera
ascendente: infraestructura, sistema operativo host y un hipervisor y, encima de todo
eso, cada máquina virtual tiene su propio sistema operativo y todas las bibliotecas
necesarias. En el caso de Docker, el servidor host solo tiene la infraestructura y el
sistema operativo y, encima de eso, el motor de contenedor, que mantiene el
contenedor aislado, pero con el uso compartido de los servicios del sistema operativo
de base.
Dado que los contenedores requieren muchos menos recursos (por ejemplo, no
necesitan un sistema operativo completo), se inician rápidamente y son fáciles de
implementar. Esto permite tener una mayor densidad, lo que significa que se pueden
ejecutar más servicios en la misma unidad de hardware, reduciendo así los costos.
El objetivo principal de una imagen es que hace que el entorno (dependencias) sea el
mismo entre las distintas implementaciones. Esto significa que puede depurarlo en su
equipo y, a continuación, implementarlo en otra máquina con el mismo entorno
garantizado.
Imagine que es responsable de enviar lotes de cartas, según proceda, para enviarlas por
correo a los clientes en papel y sobres reales, que se entregarán físicamente en la
dirección postal de cada cliente (entonces no existía el correo electrónico).
En algún momento, se da cuenta de que las cartas no son más que una composición de
un conjunto grande de párrafos, que se eligen y ordenan según proceda, según el
propósito de la carga, por lo que diseña un sistema para emitir cartas rápidamente,
esperando conseguir una increíble mejora.
El sistema es simple:
2. Para emitir un conjunto de cartas, elija las hojas con los párrafos necesarios,
apílelas y alinéelas para que queden y se lean bien.
3. Por último, colóquelas en la fotocopiadora y presione inicio para producir tantas
cartas como sean necesarias.
Puede pensar en una imagen como un disco duro de solo lectura auxiliar listo para
instalarse en un "equipo" donde el sistema operativo ya está instalado.
De forma similar, puede pensar en un contenedor como el "equipo" con el disco duro
de imagen instalado. El contenedor, como un equipo, se puede apagar o desactivar.
Anterior Siguiente
Terminología de Docker
Artículo • 20/03/2023
Sugerencia
Descargar PDF
En esta sección se enumeran los términos y las definiciones que debe conocer antes de
profundizar en el uso de Docker. Para consultar más definiciones, lea el amplio
glosario que Docker proporciona.
Dockerfile: archivo de texto que contiene instrucciones sobre cómo compilar una
imagen de Docker. Es como un script por lotes; la primera línea indica la imagen base
con la que se comienza y, después, deben seguirse las instrucciones para instalar
programas necesarios, copiar archivos, etc., hasta obtener el entorno de trabajo que se
necesita.
docker build
Etiqueta: una marca o una etiqueta que se puede aplicar a las imágenes para que se
puedan identificar diferentes imágenes o versiones de la misma imagen (según el
número de versión o el entorno de destino).
Docker Hub: registro público para cargar imágenes y trabajar con ellas. Docker Hub
proporciona hospedaje de imágenes de Docker, registros públicos o privados,
desencadenadores de compilación y enlaces web e integración con GitHub y Bitbucket.
Azure Container Registry: recurso público para trabajar con imágenes de Docker y sus
componentes en Azure. Esto proporciona un registro cercano a las implementaciones en
Azure que le proporciona control sobre el acceso, lo que le permite usar los grupos y los
permisos de Azure Active Directory.
Docker Trusted Registry (DTR) : servicio del registro de Docker (ofrecido por Docker)
que se puede instalar de forma local, por lo que se encuentra en el centro de datos y la
red de la organización. Es ideal para imágenes privadas que deben administrarse dentro
de la empresa. Docker Trusted Registry se incluye como parte del producto Docker
Datacenter.
Clúster: colección de hosts de Docker que se expone como si fuera un solo host de
Docker virtual, de manera que la aplicación se puede escalar a varias instancias de los
servicios repartidos entre varios hosts del clúster. Los clústeres de Docker se pueden
crear con Kubernetes, Azure Service Fabric, Docker Swarm y Mesosphere DC/OS.
Anterior Siguiente
Contenedores, imágenes y registros de
Docker
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Los desarrolladores deben almacenar las imágenes en un registro, que actúa como una
biblioteca de imágenes y es necesario cuando se implementa en orquestadores de
producción. Docker mantiene un registro público a través de Docker Hub ; otros
proveedores ofrecen registros para distintas colecciones de imágenes, incluido Azure
Container Registry . Como alternativa, las empresas pueden tener un registro privado
local para sus propias imágenes de Docker.
En la figura 2-4 se muestra cómo se relacionan las imágenes y los registros de Docker
con otros componentes. También se muestran las diversas ofertas de registro de los
proveedores.
Figura 2-4. Taxonomía de términos de Docker y conceptos
el registro es como una estantería donde las imágenes se almacenan y están disponibles
para extraerlas con el fin de compilar contenedores que ejecuten servicios o
aplicaciones web. Hay registros de Docker privados a nivel local y en la nube pública.
Docker Hub es que un registro público mantenido por Docker; junto con Docker Trusted
Registry, una solución a nivel empresarial, Azure ofrece Azure Container Registry. AWS,
Google y otros también tienen registros de contenedor.
Quiere tener una latencia de red mínima entre las imágenes y el entorno de
implementación elegido. Por ejemplo, si el entorno de producción es la nube de
Azure, probablemente quiera almacenar las imágenes en Azure Container
Registry , para que la latencia de red sea mínima. De forma similar, si el entorno
de producción es local, puede tener un registro de confianza de Docker local
disponible dentro de la misma red local.
Anterior Siguiente
Selección entre .NET 6 y
.NET Framework para contenedores de
Docker
Artículo • 15/02/2023
Sugerencia
Descargar PDF
Se admiten dos marcos para compilar aplicaciones de Docker contenedorizadas del lado
servidor con .NET: .NET Framework y .NET 6 . Ambos comparten muchos de los
componentes de la plataforma .NET y es posible compartir código entre ellos. Aun así,
presentan diferencias fundamentales, y la elección del marco dependerá de lo que
quiera realizar. En esta sección se proporciona orientación sobre cuándo se debe elegir
cada marco.
Anterior Siguiente
Orientación general
Artículo • 29/03/2023
Sugerencia
Descargar PDF
En resumen, al crear aplicaciones .NET en contenedores, debe optar por .NET 7 como
opción predeterminada. ya que esta opción presenta muchas ventajas y es la que mejor
se adapta a la filosofía y al estilo de trabajo de los contenedores.
Otra ventaja adicional de usar .NET 7 es que puede ejecutar versiones paralelas de .NET
para aplicaciones en la misma máquina. Esta ventaja es más importante para servidores
o máquinas virtuales que no utilizan contenedores, porque los contenedores aíslan las
versiones de .NET que necesita la aplicación. (Siempre que sean compatibles con el
sistema operativo subyacente).
Recursos adicionales
Libro electrónico: Modernize existing .NET Framework applications with Azure
and Windows Containers (Libro electrónico: Modernización de las aplicaciones
.NET Framework existentes con contenedores de Azure y de Windows)
https://aka.ms/liftandshiftwithcontainersebook
Anterior Siguiente
Cuándo elegir .NET para contenedores
de Docker
Artículo • 29/03/2023
Sugerencia
Descargar PDF
A continuación le ofrecemos una explicación más detallada sobre por qué elegir .NET 7.
Visual Studio para Mac es un IDE, una evolución de Xamarin Studio, que se ejecuta en
macOS y admite el desarrollo de aplicaciones basadas en Docker. Esta herramienta
también debe ser la opción preferida para los desarrolladores que trabajan en máquinas
Mac y que también quieran usar un IDE eficaz.
También puede usar Visual Studio Code en macOS, Linux y Windows. Visual Studio
Code es totalmente compatible con .NET 7, incluidos IntelliSense y la depuración. Como
VS Code es un editor ligero, puede usarlo para desarrollar aplicaciones en contenedor
en la máquina junto con la interfaz de la línea de comandos de Docker y la CLI de .NET.
También puede utilizar .NET 7 con la mayoría de editores de terceros, como Sublime,
Emacs, vi y el proyecto OmniSharp de código abierto, que también es compatible con
IntelliSense.
Además de los editores e IDE, puede utilizar la CLI de .NET en todas las plataformas
admitidas.
Un microservicio está pensado para ser lo más pequeño posible: que sea ligero al
acelerar, que tenga una superficie pequeña, que tenga un pequeño contexto limitado
(comprobar DDD, diseño basado en dominios ), que represente una pequeña área de
problemas y que se pueda iniciar y detener rápidamente. Para cumplir con estos
requisitos, le recomendamos que utilice imágenes de contenedor pequeñas y fáciles de
ejemplificar, como la imagen de contenedor de .NET 7.
Anterior Siguiente
Cuándo elegir .NET Framework para
contenedores de Docker
Artículo • 29/03/2023
Sugerencia
Descargar PDF
Mientras que .NET 7 ofrece ventajas significativas para las aplicaciones nuevas y los
patrones de aplicación, .NET Framework continuará siendo una buena elección para
muchos escenarios existentes.
Sin embargo, incluso con ese avance excepcional desde .NET Standard 2.0 y
.NET Core 2.1 o versiones posteriores, puede haber casos en los que ciertos paquetes
NuGet necesiten Windows para ejecutarse, y puede que no admitan .NET Core o
versiones posteriores. Si los paquetes son críticos para la aplicación, entonces debe usar
.NET Framework en los contenedores de Windows.
ASP.NET Web Forms. Esta tecnología solo está disponible en .NET Framework.
Actualmente no está previsto migrar ASP.NET Web Forms a .NET o versiones
posteriores.
Recursos adicionales
Aspectos básicos de .NET
https://learn.microsoft.com/dotnet/fundamentals
Anterior Siguiente
Tabla de decisiones: implementaciones
de .NET para su uso con Docker
Artículo • 29/03/2023
Sugerencia
Descargar PDF
) Importante
Anterior Siguiente
Selección del sistema operativo de
destino con contenedores de .NET
Artículo • 20/03/2023
Sugerencia
Descargar PDF
Para Windows, puede usar Windows Server Core o Nano Server de Windows. Estas
versiones de Windows proporcionan diferentes características (IIS en Windows Server
Core frente a un servidor web autohospedado, como Kestrel, en Nano Server) que .NET
Framework o .NET 7, respectivamente, podrían necesitar.
Para Linux, hay varias distribuciones disponibles y compatibles en imágenes oficiales del
Docker de .NET (por ejemplo, Debian).
También puede crear su propia imagen de Docker en los casos en que quiera utilizar
una distribución de Linux diferente o que quiera una imagen con las versiones no
proporcionadas por Microsoft. Por ejemplo, puede crear una imagen con ASP.NET Core
ejecutándose en los tradicionales .NET Framework y Windows Server Core, que no sería
un escenario tan habitual para Docker.
Imagen Comentarios
Anterior Siguiente
Imágenes oficiales de Docker de .NET
Artículo • 08/03/2023
Sugerencia
Descargar PDF
Las imágenes oficiales de Docker de .NET son imágenes de Docker que Microsoft ha
creado y optimizado. Están disponibles públicamente en el Registro de artefactos
Microsoft . Puede buscar en el catálogo para encontrar todos los repositorios de
imágenes de .NET, por ejemplo, el repositorio del SDK de .NET .
Cada repositorio puede contener varias imágenes, según las versiones de .NET y según
el sistema operativo y las versiones (Linux Debian, Linux Alpine, Windows Nano Server,
Windows Server Core, etc.). Los repositorios de imágenes ofrecen un etiquetado
exhaustivo con el que es más fácil elegir no solo la versión de un marco concreto, sino
también un sistema operativo (distribución de Linux o versión de Windows).
En producción
Lo importante en producción es la rapidez con la que se pueden implementar e iniciar
los contenedores según una imagen de .NET de producción. Por tanto, la imagen solo
en entorno de ejecución basada en mcr.microsoft.com/dotnet/aspnet:7.0 es pequeña, de
modo que puede viajar rápidamente a través de la red desde el registro de Docker hasta
los hosts de Docker. El contenido está listo para ejecutarse, lo que agiliza el proceso que
va desde iniciar el contenedor hasta procesar los resultados. En el modelo de Docker, no
es necesario compilar desde el código C#, como cuando se ejecuta dotnet build o
dotnet publish al usar el contenedor de compilación.
En esta imagen optimizada solo se colocan los archivos binarios y otros contenidos
necesarios para ejecutar la aplicación. Por ejemplo, el contenido que crea dotnet
publish solo contiene los archivos binarios de .NET compilados, las imágenes y los
archivos .js y .css. Con el tiempo, verá imágenes que contienen paquetes anteriores a la
compilación (la compilación del lenguaje intermedio al nativo que se produce en tiempo
de ejecución).
Aunque hay varias versiones de las imágenes de .NET y ASP.NET Core, todas ellas
comparten una o más capas, incluida la capa base. Por tanto, la cantidad de espacio en
disco necesaria para almacenar una imagen es pequeña; consiste únicamente en las
diferencias entre la imagen personalizada y su imagen base. El resultado es que es
rápido extraer la imagen desde el Registro.
Imagen Comentarios
Anterior Siguiente
Diseño de la arquitectura de
aplicaciones basadas en contenedores y
microservicios
Artículo • 29/03/2023
Sugerencia
Descargar PDF
Los microservicios ofrecen grandes ventajas, pero también generan nuevos desafíos
enormes. Los patrones de arquitectura de microservicios son los pilares fundamentales a la
hora de crear una aplicación basada en microservicios.
Previamente en esta guía, ha aprendido los conceptos básicos sobre los contenedores y
Docker. Esta era la información mínima necesaria para empezar a trabajar con
contenedores. A pesar de que los contenedores posibilitan los microservicios y
funcionan muy bien con estos, no son obligatorios para una arquitectura de
microservicios. Muchos conceptos arquitectónicos presentes en esta sección se podrían
aplicar sin contenedores. A pesar de ello, esta guía se centra en la intersección de
ambos debido a la importancia actual de los contenedores.
Anterior Siguiente
Incluir en un contenedor aplicaciones
monolíticas
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Para administrar este modelo, debe implementar un único contenedor para representar
la aplicación. Para aumentar la capacidad, deberá escalar horizontalmente, es decir, solo
tiene que agregar más copias con un equilibrador de carga delante. La simplicidad
proviene de administrar una única implementación en un solo contenedor o máquina
virtual.
Figura 4-1. Ejemplo de la arquitectura de una aplicación monolítica en contenedores
Por ejemplo, en una aplicación típica de comercio electrónico, es probable que deba
escalar el subsistema de información del producto, dado que muchos más clientes
examinan los productos en lugar de comprarlos. Más clientes usan la cesta en lugar de
usar la canalización de pago. Menos clientes publican comentarios o consultan su
historial de compras. Y es posible que solo un grupo reducido de empleados deba
administrar el contenido y las campañas de marketing. Si escala el diseño monolítico,
todo el código para estas distintas tareas se implementa varias veces y se escala al
mismo nivel.
Figura 4-2. Enfoque monolítico: el host ejecuta varias aplicaciones, cada aplicación se
ejecuta como un contenedor
Figura 4-3. Ejemplo de varios hosts que escalan verticalmente una sola aplicación de
contenedor
Dado que los contenedores son inmutables por diseño, nunca debe preocuparse por
máquinas virtuales dañadas. En cambio, los scripts de actualización para una máquina
virtual podrían olvidar tener en cuenta algún archivo o configuración concreto que se
haya quedado en el disco.
Como también se muestra en la figura 4-4, el flujo de publicación inserta una imagen a
través de un registro de contenedor. Puede ser Azure Container Registry (un registro
cercano a las implementaciones en Azure y protegido por las cuentas y los grupos de
Azure Active Directory) o cualquier otro registro de Docker, como Docker Hub o un
registro local.
Anterior Siguiente
Administración de estado y datos en
aplicaciones de Docker
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Los montajes tmpfs son como carpetas virtuales que solo existen en la memoria
del host y nunca se escriben en el sistema de archivos.
Bases de datos relacionales remotas como Azure SQL Database , bases de datos
NoSQL como Azure Cosmos DB o servicios de caché como Redis .
Sin embargo, el uso de los volúmenes de Docker es ahora la mejor manera de controlar
datos locales en Docker. Si necesita obtener más información sobre el almacenamiento
en contenedores, consulte Docker storage drivers (Controladores de almacenamiento
de Docker) y About storage drivers (Sobre los controladores de almacenamiento).
Los volúmenes son directorios asignados desde el sistema operativo del host a
directorios en contenedores. Cuando el código en el contenedor tiene acceso al
directorio, ese acceso es realmente a un directorio en el sistema operativo del host. Este
directorio no está asociado a la duración del contenedor y Docker lo administra y aísla
de la funcionalidad básica de la máquina host. Por tanto, los volúmenes de datos están
diseñados para conservar los datos independientemente de la vida del contenedor. Si
elimina un contenedor o una imagen del host de Docker, los datos que se conservan en
el volumen de datos no se eliminan.
Los volúmenes pueden tener nombre o ser anónimos (predeterminado). Los volúmenes
con nombre son la evolución de los Contenedores de volúmenes de datos y facilitan el
uso compartido de datos entre contenedores. Los volúmenes también admiten
controladores de volumen, que le permiten almacenar datos en hosts remotos, entre
otras opciones.
Los montajes de enlace están disponibles desde hace mucho tiempo y permiten la
asignación de cualquier carpeta a un punto de montaje en un contenedor. Los montajes
de enlace tienen más limitaciones que los volúmenes y algunos problemas de seguridad
importantes, por lo que los volúmenes son la opción recomendada.
Los montajes tmpfs son básicamente carpetas virtuales que solo existen en la memoria
del host y nunca se escriben en el sistema de archivos. Son rápidos y seguros, pero usan
memoria y solo están diseñados para datos temporales y no persistentes.
Tal como se muestra en la figura 4-5, los volúmenes de Docker normales pueden
almacenarse fuera de los propios contenedores, pero dentro de los límites físicos del
servidor de host o de la máquina virtual. Pero los contenedores de Docker no pueden
acceder a un volumen desde un servidor de host o máquina virtual a otro. En otras
palabras, con estos volúmenes, no es posible administrar los datos que se comparten
entre contenedores que se ejecutan en otros hosts de Docker, aunque se podría lograr
con un controlador de volumen que sea compatible con los hosts remotos.
Los volúmenes se pueden compartir entre contenedores, pero solo en el mismo host, a
menos que use un controlador remoto compatible con hosts remotos. Además, cuando
un orquestador administra los contenedores de Docker, estos podrían "moverse" entre
hosts, según las optimizaciones que el clúster realice. Por tanto, no se recomienda usar
volúmenes de datos para los datos empresariales. Pero son un buen mecanismo para
trabajar con archivos de seguimiento, archivos temporales o similares que no afectarán
a la coherencia de los datos empresariales.
Las herramientas de orígenes de datos remotos y caché como Azure SQL Database,
Azure Cosmos DB o una caché remota como Redis pueden usarse en aplicaciones en
contenedores del mismo modo que se usan al desarrollar sin contenedores. Se trata de
una manera comprobada para almacenar datos de aplicaciones empresariales.
Bases de datos relacionales y bases de datos NoSQL. Hay muchas opciones para bases
de datos externas, desde bases de datos relacionales como SQL Server, PostgreSQL u
Oracle hasta bases de datos NoSQL como Azure Cosmos DB, MongoDB, etc. Estas bases
de datos no se van a explicar en esta guía porque pertenecen a un tema completamente
diferente.
Anterior Siguiente
Arquitectura orientada a servicios
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Estos servicios ahora se pueden implementar como contenedores de Docker, con lo que
se resuelven los problemas de implementación, puesto que todas las dependencias se
incluyen en la imagen de contenedor. Pero cuando se necesita escalar verticalmente
aplicaciones SOA, es posible que tenga problemas de escalabilidad y disponibilidad si va
a efectuar la implementación en función de hosts de Docker únicos. Aquí es donde
puede ayudarle el software de agrupación en clústeres de Docker, o un orquestador,
como se explica en secciones posteriores, donde se describen los enfoques de
implementación para microservicios.
Los contenedores de Docker son útiles (pero no obligatorios) para las arquitecturas
orientadas a servicios tradicionales y las arquitecturas de microservicios más avanzadas.
Esta guía se centra en los microservicios, puesto que los enfoques SOA son menos
prescriptivos que los requisitos y técnicas empleados en una arquitectura de
microservicios. Si sabe cómo crear una aplicación basada en microservicios, también
sabrá cómo crear una aplicación orientada a servicios más sencilla.
Anterior Siguiente
Arquitectura de microservicios
Artículo • 24/03/2023
Sugerencia
Descargar PDF
En esta guía solo se cubren o introducen los tres primeros aspectos. Los dos últimos
puntos, que están relacionados con el ciclo de vida de la aplicación, se tratan en el libro
electrónico adicional Ciclo de vida de aplicaciones de Docker en contenedor con la
plataforma y las herramientas de Microsoft .
Recursos adicionales
Mark Russinovich. Microservices: An application revolution powered by the
cloud (Microservicios: Una revolución en las aplicaciones con la tecnología de la
nube)
https://azure.microsoft.com/blog/microservices-an-application-revolution-
powered-by-the-cloud/
Anterior Siguiente
Propiedad de los datos por
microservicio
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Esto significa que el modelo conceptual del dominio variará entre subsistemas o
microservicios. Piense en las aplicaciones empresariales, donde las aplicaciones de
administración de las relaciones con el cliente (CRM), los subsistemas de compras
transaccionales y los subsistemas de asistencia al cliente llaman cada uno de ellos a
datos y atributos de entidades de cliente únicos y usan un contexto enlazado diferente.
Este principio es similar en el diseño guiado por el dominio (DDD) , donde cada
contexto enlazado o subsistema o servicio autónomo debe ser propietario de su
modelo de dominio (datos más lógica y comportamiento). Cada contexto enlazado de
DDD se correlaciona con un microservicio de negocios (uno o varios servicios). En la
sección siguiente se ofrece más información sobre el patrón de contexto enlazado.
Por otro lado, el enfoque tradicional (datos monolíticos) usado en muchas aplicaciones
es tener una sola base de datos centralizada o solo algunas bases de datos. Suele ser
una base de datos SQL normalizada que se usa para toda la aplicación y todos los
subsistemas internos, como se muestra en la figura 4-7.
En el enfoque tradicional, hay una base de datos compartida en todos los servicios,
normalmente en una arquitectura en capas. En el enfoque de microservicios, cada
microservicio posee sus datos o modelos. El enfoque de la base de datos centralizada
en principio parece más sencillo y parece permitir la reutilización de entidades de
diferentes subsistemas para que todo sea coherente. Pero la realidad es que se acaban
teniendo tablas enormes que sirven a muchos subsistemas distintos e incluyen atributos
y columnas que no se necesitan en la mayoría de los casos. Es como intentar usar el
mismo mapa físico para ir de excursión un par de horas, para hacer un viaje en coche
que dure todo un día y para aprender geografía.
Una aplicación monolítica con una sola base de datos relacional presenta dos ventajas
importantes: las transacciones ACID y el lenguaje SQL, que funcionan en todas las
tablas y los datos relacionados con la aplicación. Este enfoque proporciona una manera
sencilla de escribir una consulta que combina datos de varias tablas.
Pero el acceso a los datos es mucho más complejo cuando se migra a una arquitectura
de microservicios. Incluso cuando se usan transacciones ACID dentro de un
microservicio o contexto delimitado, es fundamental tener en cuenta que los datos que
pertenecen a cada microservicio son privados para ese microservicio y que solo se debe
acceder a ellos de forma sincrónica a través de los puntos de conexión de su API (REST,
gRPC, SOAP, etc.), o bien de forma asincrónica a través de mensajería (AMQP o similar).
Si vamos aún más allá, los distintos microservicios suelen usar tipos diferentes de bases
de datos. Las aplicaciones modernas almacenan y procesan distintos tipos de datos, así
que una base de datos relacional no siempre es la mejor opción. En algunos casos de
uso, una base de datos no SQL, como Azure CosmosDB o MongoDB, podría tener un
modelo de datos más adecuado y ofrecer un mejor rendimiento y escalabilidad que una
base de datos SQL como SQL Server o Azure SQL Database. En otros casos, una base de
datos relacional sigue siendo el mejor enfoque. Por lo tanto, las aplicaciones basadas en
microservicios suelen usar una combinación de bases de datos SQL y no SQL, lo que a
veces se denomina enfoque de persistencia políglota .
Un microservicio es, por tanto, como un contexto enlazado, pero además especifica que
es un servicio distribuido. Se compila como un proceso independiente para cada
contexto enlazado y debe usar los protocolos distribuidos indicados anteriormente,
como HTTP/HTTPS, WebSockets o AMQP . Pero el patrón de contexto enlazado no
especifica si el contexto enlazado es un servicio distribuido o si es simplemente un
límite lógico (por ejemplo, un subsistema genérico) de una aplicación de
implementación monolítica.
Recursos adicionales
Chris Richardson. Pattern: Database per service (Patrón: base de datos por
servicio)
https://microservices.io/patterns/data/database-per-service.html
Sugerencia
Descargar PDF
Aquí es donde hay una diferencia entre la arquitectura lógica y la arquitectura física de
una aplicación. La arquitectura lógica y los límites lógicos de un sistema no se asignan
necesariamente uno a uno a la arquitectura física o de implementación. Esto puede
suceder, pero a menudo no es así.
Los servicios del ejemplo comparten el mismo modelo de datos porque el servicio Web
API tiene como destino los mismos datos que el servicio Search. Por tanto, en la
implementación física del microservicio empresarial, esa función se divide de manera
que pueda escalar horizontal o verticalmente cada uno de esos servicios internos según
sea necesario. Es posible que el servicio Web API normalmente necesite más instancias
que el servicio Search, o viceversa.
Anterior Siguiente
Desafíos y soluciones de la
administración de datos distribuidos
Artículo • 03/04/2023
Sugerencia
Descargar PDF
La manera en que identifica los límites entre varios contextos de aplicación con un
dominio diferente para cada contexto es exactamente cómo puede identificar los límites
de cada microservicio de negocio con sus respectivos datos y modelo de dominio.
Siempre se intenta minimizar el acoplamiento entre esos microservicios. Más adelante
en esta guía se explica con más detalle este modelo de diseño de identificación y
modelo de dominio en la sección Identificación de los límites del modelo de dominio
para cada microservicio.
Puerta de enlace de API. Para una agregación de datos simple de varios microservicios
que poseen diferentes bases de datos, el enfoque recomendado es utilizar un
microservicio de agregación conocido como puerta de enlace de API. No obstante, se
debe tener cuidado con la implementación de este patrón, ya que puede ser un punto
de obstrucción en el sistema y puede infringir el principio de autonomía de
microservicio. Para mitigar esta posibilidad, puede tener varias puertas de enlace de API
y que cada una se centre en un segmento vertical o área de negocio del sistema. El
patrón de puerta de enlace de API se detalla más adelante en la sección Puerta de
enlace de API.
Federación de GraphQL Una opción que se debe tener en cuenta si los microservicios
ya usan GraphQL es la federación de GraphQL . La federación le permite definir
"subgráficos" de otros servicios y redactarlos en un "supergráfico" agregado que actúa
como un esquema independiente.
CQRS con tablas de consulta o lectura. Otra solución para la agregación de datos de
varios microservicios es el patrón de vista materializada. En este enfoque, se genera de
antemano (se preparan los datos desnormalizados antes de que se produzcan las
consultas reales) una tabla de solo lectura con los datos que pertenecen a varios
microservicios. La tabla tiene un formato adaptado a las necesidades de la aplicación
cliente.
Piense en algo parecido a la pantalla de una aplicación móvil. Si solo tiene una base de
datos, puede reunir los datos de esa pantalla mediante una consulta SQL que realiza
una combinación compleja que implica varias tablas. Pero, si tiene varias bases de datos
y cada una pertenece a un microservicio diferente, no se puede consultar las bases de
datos y crear una instrucción join (combinación) de SQL. La consulta compleja se
convierte en un desafío. Para abordar esta necesidad, se puede usar un enfoque CQRS:
crear una tabla desnormalizada en otra base de datos que se use solo para las consultas.
La tabla se puede diseñar específicamente para los datos que necesita para la consulta
compleja, con una relación uno a uno entre los campos que son necesarios para la
pantalla de la aplicación y las columnas de la tabla de consulta. También pueden
utilizarse con fines informativos.
"Datos fríos" en bases de datos centrales. Para informes complejos y consultas que no
necesiten datos en tiempo real, un enfoque común consiste en exportar los "datos
dinámicos" (datos transaccionales de los microservicios) como "datos fríos" en grandes
bases de datos que se utilizan solo en informes. Dicho sistema de base de datos central
puede ser un sistema basado en macrodatos, como Hadoop, un almacén de datos
basado por ejemplo en Azure SQL Data Warehouse, o incluso una única base de datos
SQL utilizada solamente para informes (si el tamaño no es un problema).
Debe tenerse en cuenta que esta base de datos centralizada tan solo se utilizará para
consultas e informes que no requieran datos en tiempo real. Las actualizaciones y las
transacciones originales, como origen confiable, deben estar en los datos de
microservicios. La forma en que se sincronizarían los datos sería mediante comunicación
orientada a eventos (descrita en las secciones siguientes) o mediante otras herramientas
de importación y exportación de infraestructura de base de datos. Si se utiliza la
comunicación orientada a eventos, el proceso de integración sería similar a la manera en
que se propagan los datos como se describió anteriormente para las tablas de consulta
CQRS.
Como indica el teorema CAP , debe elegir entre disponibilidad y coherencia ACID. La
mayoría de los escenarios basados en microservicios exigen disponibilidad y
escalabilidad elevada en lugar de coherencia fuerte. Las aplicaciones críticas deben
permanecer activas y en ejecución, y los desarrolladores pueden solucionar el problema
de coherencia mediante el uso de técnicas de trabajo con coherencia débil o eventual.
Este es el enfoque adoptado por la mayoría de las arquitecturas basadas en
microservicios.
Además, las transacciones de confirmación en dos fases de estilo ACID no solo van en
contra de los principios de microservicios; la mayoría de las bases de datos NoSQL
(Azure Cosmos DB, MongoDB, etc.) no son compatibles con las transacciones de
confirmación en dos fases, típicas de los escenarios de bases de datos distribuidas. Pero
es esencial mantener la coherencia de los datos entre los servicios y las bases de datos.
Este desafío también está relacionado con la cuestión de cómo se propagan los cambios
a los distintos microservicios cuando hay datos concretos que deben ser redundantes:
por ejemplo, cuando necesite que el nombre o la descripción del producto estén en el
microservicio de catálogo y en el microservicio de cesta.
Una buena solución para este problema consiste en usar coherencia eventual entre
microservicios articulada mediante comunicación orientada a eventos y un sistema de
publicación y suscripción. Estos temas se tratan más adelante en la sección
Comunicación asincrónica orientada a eventos de esta guía.
Por ejemplo, imagine que la aplicación cliente realiza una llamada API HTTP a un
microservicio individual como el de pedidos. Si el microservicio de pedidos llama a su
vez a otros microservicios mediante HTTP en el mismo ciclo de solicitud/respuesta,
estará creando una cadena de llamadas HTTP. Aunque en un principio podría parecer
razonable, hay aspectos importantes que se deben tener en cuenta:
Por lo tanto, para aplicar el principio de autonomía de microservicio y tener una mejor
resistencia, se debería minimizar el uso de cadenas de comunicación de
solicitud/respuesta entre los microservicios. Se recomienda usar interacción asincrónica
solo para la comunicación dentro del microservicio, ya sea mediante el uso de
comunicación asincrónica basada en eventos y mensajes, o bien mediante sondeo HTTP
(asincrónico) independientemente del ciclo de solicitud/respuesta HTTP original.
El uso de comunicación asincrónica se explica con más detalle más adelante en esta
guía, en las secciones La integración asincrónica del microservicio obliga a su autonomía
y Comunicación asincrónica basada en mensajes.
Recursos adicionales
Teorema CAP
https://en.wikipedia.org/wiki/CAP_theorem
Coherencia final
https://en.wikipedia.org/wiki/Eventual_consistency
Materialized View
https://learn.microsoft.com/azure/architecture/patterns/materialized-view
Compensating Transaction
https://learn.microsoft.com/azure/architecture/patterns/compensating-transaction
Anterior Siguiente
Identificar los límites del modelo de
dominio para cada microservicio
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Otra herramienta que informa sobre su elección de diseño es la ley de Conway , que
indica que una aplicación reflejará los límites sociales de la organización que la produjo.
Pero a veces sucede lo contrario: el software forma la organización de una empresa. Tal
vez deba invertir la ley de Conway y establecer los límites de la forma que quiere que la
empresa se organice, decantándose por la consultoría de procesos empresariales.
Para identificar los contextos limitados, puede usar un patrón DDD denominado patrón
de asignación de contexto . Con la asignación de contexto, puede identificar los
distintos contextos de la aplicación y sus límites. Es habitual tener un contexto y un
límite diferentes para cada subsistema pequeño, por ejemplo. La asignación de contexto
es una manera de definir y establecer explícitamente esos límites entre dominios. Un BC
es autónomo, incluye los detalles de un único dominio, como las entidades de dominio,
y define los contratos de integración con otros BC. Esto es similar a la definición de un
microservicio: es autónomo, implementa cierta capacidad de dominio y debe
proporcionar interfaces. Esta es la razón por la que la asignación de contexto y el patrón
de contexto limitado son enfoques excelentes para identificar los límites del modelo de
dominio de sus microservicios.
Figura 4-10. Identificación de las entidades y de los límites del modelo de microservicio
Sin embargo, también puede tener entidades que tienen una forma diferente, pero
comparten la misma identidad a través de los múltiples modelos de dominio de los
diversos microservicios. Por ejemplo, la entidad User se identifica en el microservicio de
administración de conferencias. Ese mismo usuario, con la misma identidad, es el que se
llama compradores en el microservicio de pedidos, o el que se llama pagador en el
microservicio de pago e incluso el que se llama cliente en el microservicio de servicio al
cliente. Esto es porque, según el lenguaje ubicuo que cada experto en dominios use,
un usuario podría tener una perspectiva distinta incluso con atributos diferentes. La
entidad de usuario en el modelo de microservicio denominado Administración de
conferencias podría tener la mayoría de sus atributos de datos personales. Sin embargo,
puede ser que ese mismo usuario en la forma de pagador en el microservicio de pago o
en la forma de cliente en el microservicio de servicio al cliente no necesite la misma lista
de atributos.
Anterior Siguiente
Diferencias entre el patrón de puerta de
enlace de API y la comunicación directa
de cliente a microservicio
Artículo • 09/05/2023
Sugerencia
Descargar PDF
En este enfoque, cada microservicio tiene un punto de conexión público, a veces con un
puerto TCP distinto para cada microservicio. La siguiente dirección URL de Azure sería
un ejemplo de URL de un servicio determinado:
http://eshoponcontainers.westus.cloudapp.azure.com:88/
Interactuar con varios microservicios para crear una única pantalla de interfaz de usuario
aumenta el número de recorridos de ida y vuelta a través de Internet. Este enfoque
aumenta la latencia y la complejidad en el lado de la interfaz de usuario. Idealmente, las
respuestas se deberían agregar eficazmente en el lado del servidor. Este enfoque reduce
la latencia, ya que varios fragmentos de datos regresan en paralelo, y alguna interfaz de
usuario puede mostrar los datos en cuanto estén listos.
¿Cómo pueden las aplicaciones cliente comunicarse con servicios que usan
protocolos no compatible con Internet?
Los protocolos usados en el lado del servidor (por ejemplo, AMQP o protocolos
binarios) no se admiten en aplicaciones cliente. Por lo tanto, las solicitudes deben
realizarse a través de protocolos como HTTP/HTTPS y convertirse posteriormente a los
demás protocolos. Un enfoque man-in-the-middle puede ser útil en esta situación.
¿Cómo se puede dar forma a una fachada creada especialmente para las
aplicaciones móviles?
Por lo tanto, la puerta de enlace de API se encuentra entre las aplicaciones cliente y los
microservicios. Actúa como un proxy inverso, enrutando las solicitudes de los clientes a
los servicios. También puede proporcionar otras características transversales adicionales,
como autenticación, terminación SSL y caché.
La figura 4-13 muestra el encaje de una puerta de enlace de API personalizada en una
arquitectura basada en microservicios simplificada con solo algunos microservicios.
Figura 4-13. Uso de una puerta de enlace de API implementada como un servicio
personalizado
Las aplicaciones se conecten a un único punto de conexión (la puerta de enlace de API)
configurado para reenviar solicitudes a los microservicios individuales. En este ejemplo,
la puerta de enlace de API se implementa como un servicio ASP.NET Core WebHost
personalizado que se ejecuta como un contenedor.
Es importante resaltar que en ese diagrama se usa un único servicio de puerta de enlace
de API personalizado con conexión a varias aplicaciones cliente distintas. Ese hecho
puede suponer un riesgo importante porque el servicio de puerta de enlace de API irá
creciendo y evolucionando en función de los muchos requisitos de las aplicaciones
cliente. Finalmente, se verá sobredimensionado debido a las distintas necesidades y, en
la práctica, podría ser similar a una aplicación o un servicio monolíticos. Por eso es muy
recomendable dividir la puerta de enlace de API en varios servicios o varias puertas de
enlace de API más pequeñas, por ejemplo, una por cada tipo de factor de forma de
aplicación cliente.
Debe tener cuidado al implementar el patrón de puerta de enlace de API. No suele ser
una buena idea de tener una única puerta de enlace de API en la que se agreguen todos
los microservicios internos de la aplicación. Si es así, actúa como un orquestador o
agregador monolítico e infringe la autonomía de los microservicios al acoplarlos todos.
Por lo tanto, las puertas de enlace de API se deberían segregar en función de los límites
del negocio y las aplicaciones cliente no deberían actuar como un simple agregador
para todos los microservicios internos.
Figura 4-13.1 que muestra puertas de enlace de API personalizadas, segregadas por tipo
de cliente; una para los clientes móviles y otra para los clientes web. Una aplicación web
tradicional se conecta a un microservicio MVC que usa la puerta de enlace de API web.
En el ejemplo se muestra una arquitectura simplificada con varias puertas de enlace de
API específicas. En este caso, los límites identificados para cada puerta de enlace de API
se basan estrictamente en el patrón "back-end para front-end" (BFF ); por tanto, se
basan solo en la API necesaria para cada aplicación cliente. Sin embargo, en aplicaciones
más grandes, también debe ir más allá y crear otras puertas de enlace de API basadas
en los límites del negocio como un segundo eje de diseño.
Dependiendo del producto de puerta de enlace de API que use, es posible que pueda
realizar esta agregación. Pero en muchos casos resulta más flexible crear microservicios
de agregación en el ámbito de la puerta de enlace de API, de manera que la agregación
se define en el código (es decir, código de C#):
Autenticación y autorización
Integración del servicio de detección
Almacenamiento en caché de respuestas
Directivas de reintento, interruptor y QoS
Limitación de velocidad
Equilibrio de carga
Registro, seguimiento, correlación
Encabezados, cadenas de consulta y transformación de notificaciones
Adición a la lista de direcciones IP permitidas
Los productos de puerta de enlace de API suelen actuar más como un proxy inverso
para la comunicación de entrada, en el que se pueden filtrar las API de los
microservicios internos y también aplicar la autorización a las API publicadas en este
nivel único.
Con Azure API Management, puede proteger sus API con una clave, un token y el
filtrado de IP. Estas características le permiten aplicar cuotas flexibles y específicas y
límites de frecuencia, modificar la forma y el comportamiento de las API mediante
directivas y mejorar el rendimiento con el almacenamiento de respuestas en caché.
En esta guía y en la aplicación de ejemplo de referencia (eShopOnContainers), nos
limitamos a una arquitectura en contenedores más sencilla y personalizada para que
pueda centrarse en los contenedores sin formato sin utilizar productos de PaaS como
Azure API Management. Pero para las grandes aplicaciones basadas en microservicios
que se implementan en Microsoft Azure, le recomendamos que valore Azure API
Management como base para las puertas de enlace de API en producción.
Ocelot
Ocelot es una puerta de enlace de API ligera, recomendada para enfoques más
simples. Ocelot es una puerta de enlace de API de código abierto basada en .NET Core
especialmente diseñada para las arquitecturas de microservicios que necesitan puntos
de entrada unificados en sus sistemas. Es ligera, rápida y escalable, y proporciona
enrutamiento y autenticación, entre muchas otras características.
Los diagramas anteriores que muestran puertas de enlace de API personalizadas que se
ejecutan en contenedores son precisamente la forma en que también puede ejecutar
Ocelot en una aplicación basada en contenedor y microservicio.
Recursos adicionales
Chris Richardson. Pattern: API Gateway / Backend for Front-End (Patrón: puerta
de enlace de API o back-end para front-end)
https://microservices.io/patterns/apigateway.html
Anterior Siguiente
Comunicación en una arquitectura de
microservicio
Artículo • 24/03/2023
Sugerencia
Descargar PDF
No existe una única solución, sino varias. Una de ellas implica aislar los microservicios de
negocios lo máximo posible. Luego se usa la comunicación asincrónica entre los
microservicios internos y se sustituye la comunicación específica típica de la
comunicación en proceso entre objetos por la comunicación general. Para ello se
agrupan las llamadas y se devuelven los datos que agregan los resultados de varias
llamadas internas al cliente.
Los dos protocolos que se usan habitualmente son respuesta-solicitud HTTP con API de
recurso (sobre todo al consultar) y mensajería asincrónica ligera al comunicar
actualizaciones en varios microservicios. Se explican más detalladamente en las
secciones siguientes.
Tipos de comunicación
El cliente y los servicios pueden comunicarse a través de muchos tipos diferentes de
comunicación, cada uno destinado a un escenario y unos objetivos distintos.
Inicialmente, esos tipos de comunicaciones se pueden clasificar en dos ejes.
Receptor único. Cada solicitud debe ser procesada por un receptor o servicio
exactamente. Un ejemplo de este tipo de comunicación es el patrón Command .
Varios receptores. Cada solicitud puede ser procesada por entre cero y varios
receptores. Este tipo de comunicación debe ser asincrónica. Un ejemplo es el
mecanismo de publicación o suscripción empleado en patrones como la
arquitectura controlada por eventos . Se basa en una interfaz de bus de eventos
o un agente de mensajes para propagar las actualizaciones de datos entre varios
microservicios mediante eventos; normalmente se implementa a través de un bus
de servicio o algún artefacto similar como Azure Service Bus mediante temas y
suscripciones.
Una aplicación basada en microservicio suele usar una combinación de estos estilos de
comunicación. El tipo más común es la comunicación de un único receptor con un
protocolo sincrónico como HTTP/HTTPS al invocar a un servicio normal HTTP Web API.
Además, los microservicios suelen usar protocolos de mensajería para la comunicación
asincrónica entre microservicios.
Resulta útil conocer estos ejes para tener claros los posibles mecanismos de
comunicación, aunque no son la preocupación más importante a la hora de compilar
microservicios. Al integrar microservicios, no son importantes ni la naturaleza
asincrónica de la ejecución de subprocesos de cliente ni la naturaleza asincrónica del
protocolo seleccionado. Lo que sí es importante es poder integrar los microservicios de
forma asincrónica a la vez que se mantiene su independencia, como se explica en la
sección siguiente.
Además, el tener dependencias HTTP entre microservicios, como al crear largos ciclos de
solicitud-respuesta con cadenas de solicitudes HTTP, como se muestra en la primera
parte de la figura 4-15, no solo hace que los microservicios no sean autónomos, sino
que también afecta a su rendimiento en cuanto alguno de los servicios de esa cadena
no funciona correctamente.
denominada User . Sin embargo, cuando necesite almacenar datos sobre el usuario en el
microservicio Ordering , lo hará como una entidad diferente denominada Buyer . La
entidad Buyer comparte la misma identidad con la entidad User original, pero podría
tener solo los atributos que necesita el dominio Ordering y no el perfil completo del
usuario.
Podría usar cualquier protocolo para comunicar y propagar datos de forma asincrónica
en microservicios para disponer de coherencia final. Como se ha mencionado, puede
usar eventos de integración con un bus de eventos o un agente de mensajes o, si no,
puede usar incluso HTTP mediante el sondeo de los demás servicios, No importa. Lo
importante es no crear dependencias sincrónicas entre los microservicios.
Además hay varios formatos de mensaje como JSON o XML, o incluso formatos binarios,
que pueden resultar más eficaces. Si el formato binario elegido no es estándar,
probablemente no sea buena idea publicar los servicios con ese formato. Puede usar un
formato no estándar para la comunicación interna entre los microservicios. Podría
hacerlo así para la comunicación entre microservicios dentro del host de Docker o el
clúster de microservicios (orquestadores de Docker, por ejemplo) o para las aplicaciones
cliente de su propiedad que se comunican con los microservicios.
El uso de servicios REST de HTTP como lenguaje de definición de interfaz ofrece algunas
ventajas. Por ejemplo, si usa metadatos de Swagger para describir la API de servicio,
puede usar herramientas que generan código auxiliar de cliente que puede detectar y
usar directamente los servicios.
Recursos adicionales
Martin Fowler. Richardson Maturity Model Descripción del modelo REST.
https://martinfowler.com/articles/richardsonMaturityModel.html
Como se muestra en la figura 4-17, la comunicación HTTP en tiempo real significa que
puede hacer que el código de servidor inserte contenido en los clientes conectados a
medida que los datos están disponibles, en lugar de hacer que el servidor espere a que
un cliente pida nuevos datos.
Figura 4-17. Comunicación de mensajes asincrónica en tiempo real uno a varios
SignalR es una buena forma de lograr una comunicación en tiempo real para insertar
contenido a los clientes desde un servidor back-end. Puesto que la comunicación es en
tiempo real, las aplicaciones cliente muestran los cambios prácticamente de forma
inmediata. Normalmente, esto se controla mediante un protocolo como WebSockets,
con muchas conexiones WebSockets (una por cliente). Un ejemplo típico es cuando un
servicio comunica un cambio en el marcador de un partido a muchas aplicaciones web
cliente a la vez.
Anterior Siguiente
Comunicación asincrónica basada en
mensajes
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Otra regla que debe intentar seguir, tanto como sea posible, es usar la mensajería
asincrónica solo entre los servicios internos y la comunicación sincrónica (como HTTP)
solo desde las aplicaciones cliente a los servicios front-end (puertas de enlace de API y
el primer nivel de microservicios).
Una vez que se inicia el envío mediante la comunicación basada en mensajes (ya sea a
través de comandos o eventos), no se debe mezclar con la comunicación sincrónica de
HTTP.
Figura 4-18. Un único microservicio en el que se recibe un mensaje asincrónico
Al usar un bus de eventos, es posible que le interese usar una capa de abstracción
(como una interfaz de bus de eventos) basada en una implementación relacionada en
las clases con código que use la API de un agente de mensajes como RabbitMQ o un
Service Bus como Azure Service Bus con Topics. Como alternativa, es posible que le
interese usar un Service Bus de nivel superior como NServiceBus , MassTransit o
Brighter para articular el bus de eventos y el sistema de publicación y suscripción.
Pero para sistemas decisivos y de producción que necesiten una gran escalabilidad, es
posible que quiera probar Azure Service Bus. Para las abstracciones generales y las
características que facilitan el desarrollo de aplicaciones distribuidas, se recomienda
evaluar otros Service Bus comerciales y de código abierto, como NServiceBus ,
MassTransit y Brighter . Por supuesto, puede crear sus propias características de
Service Bus sobre tecnologías de nivel inferior como RabbitMQ y Docker. Pero ese
trabajo podría ser muy costoso para una aplicación empresarial personalizada.
Uso de una cola transaccional (basada en DTC) como MSMQ. (Pero es un método
heredado).
Uso del patrón de bandeja de salida : una tabla de base de datos transaccional
como una cola de mensajes que será la base para un componente de creador de
eventos que creará el evento y lo publicará.
Para obtener una descripción más completa de las dificultades de este espacio, incluido
el modo en que los mensajes con datos potencialmente incorrectos pueden terminar
publicándose, consulte Plataforma de datos para cargas de trabajo críticas en Azure: se
deben procesar todos los mensajes.
Temas adicionales que se deben tener en cuenta al usar la comunicación asincrónica son
la idempotencia y la desduplicación de los mensajes. Estos temas se describen en la
sección Implementación de la comunicación basada en eventos entre microservicios
(eventos de integración) más adelante en esta guía.
Recursos adicionales
Mensajería controlada por eventos
https://patterns.arcitura.com/soa-
patterns/design_patterns/event_driven_messaging
Coherencia final
https://en.wikipedia.org/wiki/Eventual_consistency
Anterior Siguiente
Creación, desarrollo y control de
versiones de los contratos y las API de
microservicio
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Una API de microservicio es un contrato entre el servicio y sus clientes. Solo podrá
desarrollar un microservicio de forma independiente si no incumple el contrato de su
API. Por este motivo el contrato es tan importante. Cualquier cambio en el contrato
afectará a sus aplicaciones cliente o a la puerta de enlace de API.
La naturaleza de la definición de API depende del protocolo que esté utilizando. Por
ejemplo, si usa mensajería (como AMQP), la API consiste en los tipos de mensaje. Si usa
servicios HTTP y RESTful, la API consiste en las direcciones URL y los formatos JSON de
solicitud y respuesta.
Pero, aunque piense en su contrato inicial, una API de servicio debe cambiar con el
tiempo. Normalmente, cuando esto ocurre, y especialmente si la API es una API pública
utilizada por varias aplicaciones cliente, no puede forzar a todos los clientes a actualizar
la versión a su nuevo contrato de API. Lo más habitual es implementar progresivamente
nuevas versiones de un servicio, de forma que se ejecuten simultáneamente las
versiones anteriores y las nuevas de un contrato de servicio. Por tanto, es importante
contar con una estrategia para el control de versiones del servicio.
Cuando los cambios en la API son pequeños, por ejemplo, si agrega atributos o
parámetros a la API, los clientes que usen una API anterior deberán cambiar y trabajar
con la nueva versión del servicio. Usted puede proporcionar los valores
predeterminados para los atributos que falten y que sean necesarios, y los clientes
pueden pasar por alto cualquier atributo de respuesta adicional.
Por último, si utiliza una arquitectura REST, Hypermedia es la mejor solución para
controlar las versiones de los servicios y permitir las API avanzadas.
Recursos adicionales
Scott Hanselman. ASP.NET Core RESTful Web API versioning made easy (Control
de versiones simplificado de API web RESTful de ASP.NET Core)
https://www.hanselman.com/blog/ASPNETCoreRESTfulWebAPIVersioningMadeEas
y.aspx
Anterior Siguiente
Direccionabilidad de microservicios y el
Registro del servicio
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Cada microservicio tiene un nombre único (URL) que se usa para resolver su ubicación.
El microservicio debe ser direccionable en cualquier lugar donde se ejecute. Si tiene que
pensar en qué equipo se ejecuta un microservicio determinado, todo puede ir mal
rápidamente. De la misma manera que DNS resuelve una URL para un equipo en
particular, su microservicio debe tener un nombre único para que su ubicación actual
sea reconocible. Los microservicios deben tener nombres direccionables que les
permitan ser independientes de la infraestructura en que se ejecutan. Este enfoque
implica que hay una interacción entre cómo se implementa el servicio y cómo se
detecta, porque debe haber un registro del servicio . Del mismo modo, cuando se
produce un error en un equipo, el servicio del Registro debe ser capaz de indicar que el
servicio se está ejecutando.
Recursos adicionales
Chris Richardson. Pattern: Service registry (Patrón: registro de servicios)
https://microservices.io/patterns/service-registry.html
Anterior Siguiente
Creación de interfaces de usuario
compuestas basadas en microservicios
Artículo • 10/05/2023
Sugerencia
Descargar PDF
En contraste, los propios microservicios generan y componen con precisión una interfaz
de usuario compuesta. Algunos de los microservicios controlan la forma visual de áreas
específicas de la interfaz de usuario. La principal diferencia es que tiene componentes
de la interfaz de usuario cliente (por ejemplo, clases de TypeScript) basados en plantillas,
y el ViewModel de la interfaz de usuario que perfila los datos para esas plantillas
procede de cada microservicio.
Le recomendamos que use las siguientes referencias para saber más información sobre
la interfaz de usuario compuesta basada en microservicios.
Recursos adicionales
Micro front-end (blog de Martin Fowler)
https://martinfowler.com/articles/micro-frontends.html
Micro front-end (sitio de Michael Geers)
https://micro-frontends.org/
Mauro Servienti. The secret of better UI composition (El secreto de una mejor
composición de la interfaz de usuario)
https://particular.net/blog/secret-of-better-ui-composition
Anterior Siguiente
Resistencia y alta disponibilidad en
microservicios
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Además, la resistencia está relacionada con cómo deben comportarse los sistemas
basados en la nube. Como se ha mencionado, un sistema basado en la nube debe estar
preparado para los errores e intentar recuperarse automáticamente de ellos. Por
ejemplo, en caso de errores de red o de contenedor, las aplicaciones de cliente o los
servicios de cliente deben disponer de una estrategia para volver a intentar enviar
mensajes o solicitudes, ya que en muchos casos, los errores en la nube son parciales. En
la sección Implementar aplicaciones resistentes de esta guía se explica cómo controlar
errores parciales. Se describen técnicas como los reintentos con retroceso exponencial o
el patrón de interruptor en .NET mediante el uso de bibliotecas como Polly , que
ofrece una gran variedad de directivas para controlar este asunto.
Comprobaciones de estado
El estado es diferente del diagnóstico. El estado trata de cuando el microservicio
informa sobre su estado actual para que se tomen las medidas oportunas. Un buen
ejemplo es trabajar con los mecanismos de actualización e implementación para
mantener la disponibilidad. Aunque un servicio podría actualmente estar en mal estado
debido a un bloqueo de proceso o un reinicio de la máquina, puede que el servicio siga
siendo operativo. Lo último que debe hacer es realizar una actualización que empeore
esta situación. El mejor método consiste en realizar una investigación en primer lugar o
dar tiempo a que el microservicio se recupere. Los eventos de estado de un
microservicio nos ayudan a tomar decisiones informadas y, en efecto, ayudan a crear
servicios de reparación automática.
También tiene la opción de usar una biblioteca de código abierto excelente llamada
AspNetCore.Diagnostics.HealthChecks, que está disponible en GitHub y como un
paquete NuGet . Además, la biblioteca realiza comprobaciones de estado,
concretamente de dos tipos:
Es difícil que pueda resolver por su cuenta los problemas complejos que se muestran en
la figura 4-22. Los equipos de desarrollo deben centrarse en solucionar problemas
empresariales y crear aplicaciones personalizadas con enfoques basados en
microservicio. No deben centrarse en solucionar problemas de infraestructura
complejos; si fuera así, el coste de cualquier aplicación basada en microservicio sería
enorme. Por tanto, hay plataformas orientadas a microservicios, denominadas
orquestadores o clústeres de microservicio, que tratan de solucionar los problemas
complejos de crear y ejecutar un servicio y usar de forma eficaz los recursos de
infraestructura. Este enfoque reduce las complejidades de crear aplicaciones que usan
un enfoque de microservicios.
Recursos adicionales
La aplicación Twelve-Factor. XI. Logs: Treat logs as event streams (Registros:
tratar los registros como secuencias de eventos)
https://12factor.net/logs
Anterior Siguiente
Orquestar microservicios y aplicaciones
de varios contenedores para una alta
escalabilidad y disponibilidad
Artículo • 10/05/2023
Sugerencia
Descargar PDF
use un contenedor para cada instancia de servicio. Los contenedores de Docker son
"unidades de implementación" y un contenedor es una instancia de Docker. Un host
controla muchos contenedores. Parece un enfoque lógico. Pero, ¿cómo se está
administrando el equilibrio de carga de control, el enrutamiento y la orquestación de
estas aplicaciones compuestas?
No hay cuotas para el software instalado de forma predeterminada como parte de AKS.
Todas las opciones predeterminadas se implementan con el software de código abierto.
AKS está disponible en varias máquinas virtuales en Azure. Se cobra únicamente por las
instancias de proceso que se elijan, así como por los otros recursos subyacentes de la
infraestructura que se utilicen, por ejemplo, la red y el almacenamiento. No hay ningún
cargo incremental para AKS.
Recursos adicionales
Guía de inicio rápido: Implementación de un clúster de Azure Kubernetes
Service (AKS)
https://learn.microsoft.com/azure/aks/kubernetes-walkthrough-portal
Anterior Siguiente
Proceso de desarrollo de aplicaciones
basadas en Docker
Artículo • 11/02/2023
Sugerencia
Descargar PDF
Visual Studio para Mac. Se trata de un IDE, la evolución de Xamarin Studio, que se
ejecuta en macOS. Para el desarrollo en .NET 6, se requiere la versión 8.4 o posterior.
Esta herramienta también debe ser la opción preferida para los desarrolladores que
trabajan en equipos macOS y que también quieran usar un IDE eficaz.
Mediante la instalación de Docker Desktop , puede usar una sola CLI de Docker para
compilar aplicaciones para Windows y Linux.
Recursos adicionales
Visual Studio. Sitio oficial.
https://visualstudio.microsoft.com/vs/
Anterior Siguiente
Flujo de trabajo de desarrollo para
aplicaciones de Docker
Artículo • 29/03/2023
Sugerencia
Descargar PDF
Cada contenedor (una instancia de una imagen de Docker) incluye los siguientes
componentes:
Cuando se usa un enfoque de desarrollo de editor/CLI (por ejemplo, Visual Studio Code
más la CLI de Docker en macOS o Windows), es necesario conocer cada paso,
generalmente más detalladamente que si se usa Visual Studio. Para obtener más
información sobre cómo trabajar en un entorno de CLI, vea el libro electrónico
Containerized Docker Application lifecycle with Microsoft Platforms and Tools (Ciclo
de vida de las aplicaciones en contenedor de Docker con plataformas y herramientas de
Microsoft).
Al usar Visual Studio 2022, muchos de esos pasos se controlan de forma automática, lo
que mejora considerablemente la productividad. Esto es así especialmente con
Visual Studio 2022 y cuando el destino son aplicaciones de varios contenedores. Por
ejemplo, con un solo clic, Visual Studio agrega Dockerfile y el archivo docker-
compose.yml a los proyectos con la configuración de la aplicación. Al ejecutar la
Pero que Visual Studio realice esos pasos automáticamente no significa que no sea
necesario saber lo que ocurre en segundo plano con Docker. Por lo tanto, la guía
siguiente detalla cada paso.
Además, se necesita Visual Studio 2022 versión 17.0, con la carga de trabajo Desarrollo
web y .ASP NET instalada, tal como se muestra en la figura 5-2.
Figura 5-2. Selección de la carga de trabajo Desarrollo web y ASP.NET durante la
instalación de Visual Studio 2022
Recursos adicionales
Introducción a Docker Desktop para Windows
https://docs.docker.com/docker-for-windows/
Con Visual Studio y sus herramientas para Docker, esta tarea solo exige unos clics. Al
crear un proyecto en Visual Studio 2022, hay una opción denominada Habilitar
compatibilidad con Docker, como se muestra en la figura 5-3.
Esta acción agrega un Dockerfile al proyecto con la configuración necesaria y solo está
disponible en los proyectos de ASP.NET Core.
Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:7.0
ARG source
WORKDIR /app
EXPOSE 80
COPY ${source:-obj/Docker/publish} .
ENTRYPOINT ["dotnet", " MySingleContainerWebApp.dll "]
el SDK y la CLI de .NET (dotnet CLI) para compilar y ejecutar la aplicación .NET, este valor
sería diferente. La conclusión es que la línea ENTRYPOINT y otros valores pueden variar
según el lenguaje y la plataforma que se elijan para la aplicación.
Recursos adicionales
Compilación de imágenes de Docker para aplicaciones de ASP.NET Core
https://learn.microsoft.com/dotnet/core/docker/building-net-docker-images
Si especifica una etiqueta, se toma como destino una plataforma explícita, como en los
casos siguientes:
mcr.microsoft.com/dotnet/aspnet:7.0-bullseye-slim
mcr.microsoft.com/dotnet/aspnet:7.0-nanoserver-ltsc2022
Pero, si se especifica el mismo nombre de imagen, incluso con la misma etiqueta, las
imágenes multiarquitectura (como la imagen aspnet ) usan la versión de Linux o
Windows según el sistema operativo del host de Docker que se vaya a implementar,
como se muestra en el ejemplo siguiente:
mcr.microsoft.com/dotnet/aspnet:7.0
La ejecución de cada línea de comandos crea una nueva capa en el sistema de archivos
con los cambios de la anterior, por lo que, cuando se combinan, generan el sistema de
archivos resultante.
Dado que cada nueva capa "descansa" sobre la anterior y el tamaño de la imagen
resultante aumenta con cada comando, las imágenes pueden llegar a tener un gran
tamaño si tienen que incluir, por ejemplo, el SDK necesario para compilar y publicar una
aplicación.
Aquí es donde las compilaciones de varias fases entran en escena (a partir de Docker
17.05 y posterior) para hacer su magia.
La idea central es que puede separar el proceso de ejecución del Dockerfile en fases,
donde una fase es una imagen inicial seguida de uno o más comandos, y la última fase
determina el tamaño final de la imagen.
1. Usar una imagen base de SDK (no importa su tamaño), con todo lo necesario para
compilar y publicar la aplicación en una carpeta
2. Usar una imagen base pequeña de solo el entorno de ejecución y copiar la carpeta
de publicación de la fase anterior para generar una pequeña imagen final.
Dockerfile
Línea 1: Comience una fase con una imagen base "pequeña" de solo el entorno de
ejecución, denomínela base para referencia.
Línea 5: Comience una nueva fase con una imagen "grande" para compilar y
publicar. Denomínela build como referencia.
Línea 7: Hasta la línea 16, copie los archivos del proyecto .csproj a los que se hace
referencia para poder restaurar los paquetes más adelante.
Línea 17: Restaure los paquetes del proyecto Catalog.API y los proyectos a los que
se hace referencia.
Línea 18: Copie todo el árbol de directorio de la solución (excepto los archivos o
directorios incluidos en el archivo .dockerignore) en el directorio /src de la
imagen.
Línea 19: Cambie la carpeta actual al proyecto Catalog.API.
Línea 20: Compile el proyecto (y otras dependencias del proyecto) y use como
salida el directorio /app de la imagen.
Línea 22: Comience una nueva fase a partir de la compilación. Denomínela publish
como referencia.
Línea 23: Publique el proyecto (y las dependencias) y use como salida el directorio
/app de la imagen.
Línea 25: Comience una nueva fase a partir de base y denomínela final.
Así, vamos a centrarnos en la fase build, las líneas 5 y 6 son prácticamente iguales, pero
las líneas 7-17 son diferentes para cada servicio de eShopOnContainers, así que se
tienen que ejecutar cada vez, pero si ha cambiado las líneas 7-16 a:
Dockerfile
COPY . .
Luego, sería igual para cada servicio, se copiaría la solución completa y se crearía una
capa más grande pero:
2. Puesto que la imagen más grande se produce en una fase intermedia, no afecta al
tamaño final de la imagen.
La siguiente optimización importante implica al comando restore ejecutado en la línea
17, que también es diferente para cada servicio de eShopOnContainers. Si cambia esa
línea a:
Dockerfile
Restauraría los paquetes de toda la solución, pero, de nuevo, lo haría una sola vez, en
lugar de las 15 veces con la estrategia actual.
*.sln , para omitir todos los archivos de solución del árbol de la carpeta
principal
Dockerfile
Recursos adicionales
Multi-arch .NET Core images (Imágenes de .NET Core multiarquitectura).
https://github.com/dotnet/announcements/issues/14
Create a base image (Crear una imagen base) . Documentación oficial de Docker.
https://docs.docker.com/develop/develop-images/baseimages/
Como desarrollador, debe desarrollar y probar en local hasta que inserte una
característica o cambio completados en el sistema de control de código fuente (por
ejemplo, en GitHub). Esto significa que tiene que crear las imágenes de Docker e
implementar contenedores en un host de Docker local (máquina virtual de Windows o
Linux) y ejecutar, probar y depurar en esos contenedores locales.
Para crear una imagen personalizada en el entorno local mediante la CLI de Docker y el
Dockerfile, puede usar el comando docker build, como se muestra en la figura 5-5.
De forma opcional, en lugar de ejecutar directamente docker build desde la carpeta del
proyecto, primero puede generar una carpeta que se pueda implementar con las
bibliotecas de .NET y los binarios necesarios mediante la ejecución de dotnet publish y,
luego, usar el comando docker build .
específica. Puede repetir este paso para cada imagen personalizada que tenga que crear
para la aplicación de Docker compuesta.
Cuando una aplicación se compone de varios contenedores (es decir, es una aplicación
de varios contenedores), también puede usar el comando docker-compose up --build
para compilar todas las imágenes relacionadas con un solo comando al usar los
metadatos expuestos en los archivos relacionados docker-compose.yml.
yml
version: '3.4'
services:
webmvc:
image: eshop/web
environment:
- CatalogUrl=http://catalog-api
- OrderingUrl=http://ordering-api
ports:
- "80:80"
depends_on:
- catalog-api
- ordering-api
catalog-api:
image: eshop/catalog-api
environment:
- ConnectionString=Server=sqldata;Port=1433;Database=CatalogDB;…
ports:
- "81:80"
depends_on:
- sqldata
ordering-api:
image: eshop/ordering-api
environment:
- ConnectionString=Server=sqldata;Database=OrderingDb;…
ports:
- "82:80"
extra_hosts:
- "CESARDLBOOKVHD:10.0.75.1"
depends_on:
- sqldata
sqldata:
image: mcr.microsoft.com/mssql/server:latest
environment:
- SA_PASSWORD=Pass@word
- ACCEPT_EULA=Y
ports:
- "5433:1433"
Repita esta operación para cada proyecto que quiera incluir en el archivo docker-
compose.yml.
Figura 5-7. Compatibilidad agregada con Docker en Visual Studio 2022 al hacer clic con
el botón derecho en un proyecto de ASP.NET Core
Puede implementar una aplicación de varios contenedores con un único archivo docker-
compose.yml mediante el comando docker-compose up . Pero Visual Studio agrega un
grupo de ellos para que pueda reemplazar valores en función del entorno (desarrollo o
producción) y el tipo de ejecución (versión o depuración). Esta capacidad se explica en
secciones posteriores.
Consola
En este caso, el comando enlaza el puerto interno 5000 del contenedor con el puerto 80
del equipo de host. Esto significa que el host escucha en el puerto 80 y reenvía al puerto
5000 del contenedor.
Para ejecutar una aplicación de varios contenedores con la CLI de Docker, use el
comando docker-compose up . Este comando usa el archivo docker-compose.yml que
hay en el nivel de solución para implementar una aplicación de varios contenedores. La
figura 5-11 muestra los resultados de la ejecución del comando desde el directorio
principal de la solución, que contiene el archivo docker-compose.yml.
Como se ha mencionado antes, cada vez que se agrega compatibilidad con soluciones
de Docker a un proyecto de una solución, ese proyecto se configura en el archivo global
(nivel de solución) docker-compose.yml, lo que permite ejecutar o depurar la solución
completa al mismo tiempo. Visual Studio inicia un contenedor para cada proyecto que
tiene habilitada la compatibilidad con soluciones de Docker y realiza todos los pasos
internos automáticamente (dotnet publish, docker build, etc.).
Lo importante aquí es que, como se muestra en la figura 5-12, en Visual Studio 2019 hay
un comando adicional de Docker para la acción de la tecla F5. Esta opción permite
ejecutar o depurar una aplicación de varios contenedores mediante la ejecución de
todos los contenedores definidos en los archivos docker-compose.yml en el nivel de
solución. La capacidad de depurar las soluciones de varios contenedores significa que
puede establecer varios puntos de interrupción, cada uno en un proyecto diferente
(contenedor) y, durante la depuración desde Visual Studio, detenerse en los puntos de
interrupción definidos en los distintos proyectos y en ejecución en contenedores
diferentes.
Recursos adicionales
Implementación de un contenedor de ASP.NET en un host remoto de Docker
https://learn.microsoft.com/visualstudio/containers/hosting-web-apps-in-docker
También puede probar la aplicación con la CURL del terminal, como se muestra en la
figura 5-14. En una instalación de Docker en Windows, el valor predeterminado de la IP
del host de Docker es siempre 10.0.75.1, además de la dirección IP real del equipo.
Figura 5-14. Ejemplo de prueba de la aplicación de Docker en local mediante CURL
Recursos adicionales
Inicio rápido: Docker en Visual Studio.
https://learn.microsoft.com/visualstudio/containers/container-tools
Además, debe realizar el paso 2 (agregar compatibilidad con Docker a los proyectos)
una sola vez. Por lo tanto, el flujo de trabajo es similar a las tareas de desarrollo
habituales cuando se usa .NET para cualquier otro desarrollo. Debe saber qué está
sucediendo en segundo plano (el proceso de compilación de imágenes, qué imágenes
base usa, la implementación de contenedores, etc.) y, a veces, también debe editar el
Dockerfile o el archivo docker-compose.yml para personalizar comportamientos. Pero
con Visual Studio se simplifica enormemente la mayor parte del trabajo, lo que mejora
mucho la productividad.
Dockerfile
FROM mcr.microsoft.com/windows/servercore
LABEL Description="IIS" Vendor="Microsoft" Version="10"
RUN powershell -Command Add-WindowsFeature Web-Server
CMD [ "ping", "localhost", "-t" ]
En este caso se usa una imagen base de Windows Server Core (el valor FROM) y se
instala IIS con un comando de PowerShell (el valor RUN). Del mismo modo, también se
pueden usar comandos de PowerShell para configurar otros componentes como
ASP.NET 4.x, .NET Framework 4.6 o cualquier otro software de Windows. Por ejemplo, el
siguiente comando en un Dockerfile configura ASP.NET 4.5:
Dockerfile
Recursos adicionales
aspnet-docker/Dockerfile. Comandos de PowerShell de ejemplo para ejecutar
desde Dockerfiles a fin de incluir características de Windows.
https://github.com/Microsoft/aspnet-docker/blob/master/4.7.1-
windowsservercore-ltsc2016/runtime/Dockerfile
Anterior Siguiente
Diseñar y desarrollar aplicaciones .NET
basadas en varios contenedores y
microservicios
Artículo • 29/03/2023
Sugerencia
Descargar PDF
Pero si sabe cómo diseñar y desarrollar una aplicación basada en microservicios que
también se base en contenedores de Docker, podrá diseñar y desarrollar cualquier
modelo de aplicación más sencillo. Por ejemplo, podría diseñar una aplicación de tres
niveles que también requiera un enfoque de varios contenedores. Debido a eso, y dado
que las arquitecturas de microservicios son una tendencia importante en el mundo de
los contenedores, esta sección se centra en la implementación de una arquitectura de
microservicios con contenedores de Docker.
Anterior Siguiente
Diseño de una aplicación orientada a
microservicios
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Esta sección se centra en desarrollar una hipotética aplicación empresarial del lado
servidor.
Especificaciones de la aplicación
La aplicación hipotética controla las solicitudes mediante la ejecución de lógica de
negocios, el acceso a bases de datos y, después, la devolución de respuestas HTML,
JSON o XML. Diremos que la aplicación debe admitir varios clientes, incluidos
exploradores de escritorio que ejecuten aplicaciones de página única (SPA), aplicaciones
web tradicionales, aplicaciones web móviles y aplicaciones móviles nativas. También es
posible que la aplicación exponga una API para el consumo de terceros. También debe
ser capaz de integrar sus microservicios o aplicaciones externas de forma asincrónica,
para que ese enfoque ayude a la resistencia de los microservicios en caso de errores
parciales.
Los nuevos miembros del equipo deben ser productivos con rapidez y la aplicación
debe ser fácil de entender y modificar.
Los microservicios se comunican mediante protocolos como HTTP (REST), pero también
de forma asincrónica (por ejemplo, mediante AMQP) siempre que sea posible, en
especial al propagar actualizaciones con eventos de integración.
Cada microservicio tiene su propia base de datos, lo que permite separarlo totalmente
de otros microservicios. Cuando sea necesario, la coherencia entre las bases de datos de
los diferentes microservicios se logra mediante eventos de integración de nivel de
aplicación (a través de un bus de eventos lógicos), como se controla en Command and
Query Responsibility Segregation (CQRS). Por ese motivo, las restricciones de negocio
deben adoptar la coherencia final entre los múltiples microservicios y bases de datos
relacionadas.
En el diagrama anterior se muestra que los clientes móviles y SPA se comunican con los
puntos de conexión de puerta de enlace de API única y, a continuación, se comunican
con los microservicios. Los clientes web tradicionales se comunican con el microservicio
MVC, que se comunica con microservicios mediante la puerta de enlace de API.
Por tanto, las unidades de implementación de los microservicios (e incluso de las bases
de datos de esta aplicación) son contenedores de Docker y la aplicación de referencia es
una aplicación de varios contenedores que se rige por los principios de los
microservicios.
Recursos adicionales
Repositorio de GitHub de eShopOnContainers. Código fuente de la aplicación de
referencia
https://aka.ms/eShopOnContainers/
Ventajas de una solución basada en
microservicios
Una solución basada en microservicios como esta tiene muchas ventajas:
Los contenedores se crean con rapidez, lo que permite que los desarrolladores
sean más productivos.
Un IDE como Visual Studio puede cargar proyectos más pequeños con rapidez,
aumentando la productividad de los desarrolladores.
El trabajo de desarrollo se puede dividir entre varios equipos. Cada servicio puede ser
propiedad de un único equipo de desarrollo. Cada equipo puede administrar,
desarrollar, implementar y escalar su servicio de forma independiente a los demás
equipos.
Se pueden usar las tecnologías más recientes. Como es posible empezar a desarrollar
los servicios de forma independiente y ejecutarlos en paralelo (gracias a los
contenedores y .NET), se pueden usar las tecnologías y plataformas más modernas de
forma oportuna en lugar de atascarse en una pila o marco antiguo para toda la
aplicación.
Otra desventaja con este enfoque directo de cliente a servicio es que resulta difícil
refactorizar los contratos para esos microservicios. Con el tiempo, es posible que a los
desarrolladores les interese cambiar la forma en que el sistema se divide en servicios.
Por ejemplo, es posible que combinen dos servicios o dividan uno en dos o más
servicios. Pero si los clientes se comunican directamente con los servicios, realizar este
tipo de refactorización puede interrumpir la compatibilidad con las aplicaciones cliente.
Otra razón para usar una tecnología distinta por microservicio podría ser la naturaleza
de cada microservicio. Por ejemplo, podría ser mejor usar un lenguaje de programación
funcional como F#, o incluso un lenguaje como R si los dominios de destino son de IA y
aprendizaje automático, en lugar de un lenguaje de programación más orientado a
objetos como C#.
La conclusión es que cada microservicio puede tener una arquitectura interna diferente
basada en patrones de diseño diferentes. Para evitar la ingeniería excesiva de los
microservicios, no todos deben implementarse mediante patrones de DDD avanzados.
Del mismo modo, los microservicios complejos con lógica de negocios cambiante no
deberían implementarse como componentes CRUD o el resultado sería código de baja
calidad.
Tradicional de N capas.
También puede compilar microservicios con muchas tecnologías y lenguajes, como las
de ASP.NET Core Web API, NancyFx, ASP.NET Core SignalR (disponible con .NET Core 2
o versiones posteriores), F#, Node.js, Python, Java, C++, GoLang y más.
Los patrones de varias arquitecturas y los microservicios políglotas implican que puede
mezclar y adaptar lenguajes y tecnologías a las necesidades de cada microservicio y
permitir que se sigan comunicando entre sí. Como se muestra en la figura 6-3, en las
aplicaciones formadas por muchos microservicios (contextos delimitados en
terminología del diseño controlado por dominios, o simplemente "subsistemas" como
microservicios autónomos), podría implementar cada microservicio de forma diferente.
Cada uno de ellos podría tener un modelo arquitectónico diferente y usar otros
lenguajes y bases de datos según la naturaleza de la aplicación, los requisitos
empresariales y las prioridades. En algunos casos, es posible que los microservicios sean
similares. Pero eso no es lo habitual, porque el límite del contexto y los requisitos de
cada subsistema suelen ser diferentes.
Por ejemplo, para una aplicación de mantenimiento CRUD simple, es posible que no
tenga sentido diseñar e implementar patrones de DDD. Pero para el dominio o el
negocio principal, es posible que tenga que aplicar patrones más avanzados para
abordar la complejidad empresarial con reglas de negocio cambiantes.
Anterior Siguiente
Creación de un microservicio CRUD
sencillo controlado por datos
Artículo • 29/03/2023
Sugerencia
Descargar PDF
En esta sección se describe cómo crear un microservicio sencillo que lleve a cabo
operaciones de creación, lectura, actualización y eliminación (CRUD) en un origen de
datos.
Tenga en cuenta que ejecutar un servidor de base de datos como SQL Server en un
contenedor de Docker es muy útil para entornos de desarrollo, porque puede poner en
marcha todas sus dependencias sin tener que proporcionar una base de datos local o en
la nube. Este enfoque resulta útil al ejecutar pruebas de integración. Pero no se
recomienda ejecutar un servidor de base de datos en un contenedor para entornos de
producción, ya que normalmente no se obtiene alta disponibilidad con ese método. En
un entorno de producción de Azure, le recomendamos que utilice la base de datos SQL
de Azure o cualquier otra tecnología de base de datos que pueda proporcionar alta
disponibilidad y alta escalabilidad. Por ejemplo, para un enfoque NoSQL, es posible que
elija CosmosDB.
Para crear un proyecto de API web de ASP.NET Core, seleccione primero una aplicación
web de ASP.NET Core y, después, seleccione el tipo de API. Después de crear el
proyecto, puede implementar los controladores MVC como lo haría en cualquier otro
proyecto de API Web, mediante la API de Entity Framework u otra API. En un nuevo
proyecto de API Web, puede ver que la única dependencia que tiene de ese
microservicio es el mismo ASP.NET Core. Internamente, dentro de la dependencia
Microsoft.AspNetCore.All, hace referencia a Entity Framework y a muchos otros paquetes
NuGet de .NET, como se muestra en la figura 6-7.
Figura 6-7. Dependencias en un microservicio API Web de CRUD sencillo
El modelo de datos
C#
También necesita un DbContext que represente una sesión con la base de datos. Para el
microservicio de catálogo, la clase CatalogContext se deriva de la clase base DbContext,
tal como se muestra en el ejemplo siguiente:
C#
public class CatalogContext : DbContext
{
public CatalogContext(DbContextOptions<CatalogContext> options) :
base(options)
{ }
public DbSet<CatalogItem> CatalogItems { get; set; }
public DbSet<CatalogBrand> CatalogBrands { get; set; }
public DbSet<CatalogType> CatalogTypes { get; set; }
C#
[Route("api/v1/[controller]")]
public class CatalogController : ControllerBase
{
private readonly CatalogContext _catalogContext;
private readonly CatalogSettings _settings;
private readonly ICatalogIntegrationEventService
_catalogIntegrationEventService;
public CatalogController(
CatalogContext context,
IOptionsSnapshot<CatalogSettings> settings,
ICatalogIntegrationEventService catalogIntegrationEventService)
{
_catalogContext = context ?? throw new
ArgumentNullException(nameof(context));
_catalogIntegrationEventService = catalogIntegrationEventService
?? throw new
ArgumentNullException(nameof(catalogIntegrationEventService));
_settings = settings.Value;
context.ChangeTracker.QueryTrackingBehavior =
QueryTrackingBehavior.NoTracking;
}
// GET api/v1/[controller]/items[?pageSize=3&pageIndex=10]
[HttpGet]
[Route("items")]
[ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>),
(int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(IEnumerable<CatalogItem>),
(int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> ItemsAsync(
[FromQuery]int pageSize = 10,
[FromQuery]int pageIndex = 0,
string ids = null)
{
if (!string.IsNullOrEmpty(ids))
{
var items = await GetItemsByIdsAsync(ids);
if (!items.Any())
{
return BadRequest("ids value invalid. Must be comma-
separated list of numbers");
}
return Ok(items);
}
itemsOnPage = ChangeUriPlaceholder(itemsOnPage);
return Ok(model);
}
//...
}
Guardado de datos
Los datos se crean, se eliminan y se modifican en la base de datos mediante instancias
de las clases de entidad. Puede agregar código similar al siguiente ejemplo codificado
de forma rígida (datos simulados, en este caso) a sus controladores de la API web.
C#
Una opción importante que hay que configurar en el proyecto de Web API es el registro
de la clase DbContext en el contenedor de IoC del servicio. Normalmente se hace en el
archivo Program.cs, llamando al método
builder.Services.AddDbContext<CatalogContext>() , tal como se muestra en el siguiente
ejemplo simplificado:
C#
// Additional code...
builder.Services.AddDbContext<CatalogContext>(options =>
{
options.UseSqlServer(builder.Configuration["ConnectionString"],
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(
typeof(Program).GetTypeInfo().Assembly.GetName().Name);
Recursos adicionales
Consulta de datos
https://learn.microsoft.com/ef/core/querying/index
Guardado de datos
https://learn.microsoft.com/ef/core/saving/index
JSON
{
"ConnectionString": "Server=tcp:127.0.0.1,5433;Initial
Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=
[PLACEHOLDER]",
"ExternalCatalogBaseUrl": "http://host.docker.internal:5101",
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
yml
# docker-compose.override.yml
#
catalog-api:
environment:
-
ConnectionString=Server=sqldata;Database=Microsoft.eShopOnContainers.Service
s.CatalogDb;User Id=sa;Password=[PLACEHOLDER]
# Additional environment variables for this service
ports:
- "5101:80"
Los archivos docker-compose.yml en el nivel de solución no solo son más flexibles que
los archivos de configuración en el nivel de proyecto o de microservicio, sino que
también son más seguros si reemplaza las variables de entorno declaradas en los
archivos docker-compose con valores establecidos en las herramientas de
implementación, como las tareas de implementación del Docker de Azure DevOps
Services.
Por último, puede obtener ese valor del código mediante builder.Configuration\
["ConnectionString"\] , tal como se muestra en un ejemplo de código anterior.
Pero, en entornos de producción, puede ser que le interese analizar otras formas de
almacenar secretos, como las cadenas de conexión. Una manera excelente de
administrar los secretos de aplicación consiste en usar Azure Key Vault .
Azure Key Vault ayuda a almacenar y proteger las claves criptográficas y los secretos que
usan la aplicaciones y los servicios en la nube. Un secreto es todo aquello sobre lo que
quiera mantener un control estricto, como las claves de API, las cadenas de conexión, las
contraseñas, etc. Asimismo, un control estricto incluye el registro del uso, el
establecimiento de la caducidad y la administración del acceso, entre otros aspectos.
Azure Key Vault permite un nivel de control detallado del uso de secretos de la
aplicación sin necesidad de que otras personas los conozcan. Incluso se puede definir
que los secretos vayan rotando para mejorar la seguridad sin interrumpir las
operaciones ni el desarrollo.
Puede consultar la documentación de conceptos de Key Vault para obtener más detalles.
El control de versiones permite que una API web indique las características y los
recursos que expone. De este modo, una aplicación cliente puede enviar solicitudes a
una versión específica de una característica o de un recurso. Existen varios enfoques
para implementar el control de versiones:
C#
[Route("api/v1/[controller]")]
public class CatalogController : ControllerBase
{
// Implementation ...
Este mecanismo de control de versiones es sencillo y depende del servidor que enruta la
solicitud al punto de conexión adecuado. Pero para utilizar un control de versiones más
sofisticado y adoptar el mejor método al utilizar REST, debe usar hipermedia e
implementar HATEOAS (hipertexto como motor del estado de la aplicación).
Recursos adicionales
Control de versiones de API de ASP.NET \ https://github.com/dotnet/aspnet-api-
versioning
Scott Hanselman. ASP.NET Core RESTful Web API versioning made easy (Control
de versiones simplificado de API web RESTful de ASP.NET Core)
https://www.hanselman.com/blog/ASPNETCoreRESTfulWebAPIVersioningMadeEas
y.aspx
Azure App Service Logic Apps. También puede utilizar e integrar automáticamente
su API en una Azure App Service Logic App, aunque no tenga conocimientos de
programación.
Microsoft Flow, PowerApps y Azure Logic Apps usan los metadatos de Swagger para
aprender a usar las API y conectarse a ellas.
Hay varias opciones para automatizar la generación de metadatos de Swagger para las
aplicaciones de API REST de ASP.NET Core, en forma de páginas de ayuda de API
funcionales, basadas en swagger-ui.
Esto significa que puede complementar su API con una bonita interfaz de usuario de
descubrimiento para ayudar a los desarrolladores a usar su API. Para ello se requiere una
cantidad pequeña de código y mantenimiento, puesto que se genera automáticamente,
lo que le permite centrarse en la creación de la API. El resultado para el explorador de
API se parece a la Figura 6-8.
Después de instalar estos paquetes NuGet en el proyecto de Web API, debe configurar
Swagger en la clase Program.cs, como en el siguiente código simplificado:
C#
builder.Services.AddSwaggerGen(options =>
{
options.DescribeAllEnumsAsStrings();
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "eShopOnContainers - Catalog HTTP API",
Version = "v1",
Description = "The Catalog Microservice HTTP API. This is a Data-
Driven/CRUD microservice sample"
});
});
app.UseSwagger()
.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
```
Once this is done, you can start your application and browse the following
Swagger JSON and UI endpoints using URLs like these:
```console
http://<your-root-url>/swagger/v1/swagger.json
http://<your-root-url>/swagger/
Anteriormente, vio la interfaz de usuario generada creada por Swashbuckle para una
dirección URL como http://<your-root-url>/swagger . En la figura 6-9 también puede
ver cómo se puede probar cualquier método de API.
Figura 6-9. Interfaz de usuario de Swashbuckle poniendo a prueba el método de API de
catálogo o elementos
Recursos adicionales
Páginas de ayuda de ASP.NET Core Web API con Swagger
https://learn.microsoft.com/aspnet/core/tutorials/web-api-help-pages-using-
swagger
Anterior Siguiente
Definir una aplicación de varios
contenedores con docker-compose.yml
Artículo • 09/05/2023
Sugerencia
Descargar PDF
Básicamente define cada uno de los contenedores que quiere implementar, además de
ciertas características para cada implementación de contenedor. Una vez que tenga un
archivo de descripción de la implementación de varios contenedores, puede
implementar la solución completa en una sola acción organizada por el comando de la
CLI docker-compose up o bien puede implementarla de forma transparente en Visual
Studio. En caso contrario, tendría que usar la CLI de Docker para implementar uno a uno
los contenedores en varios pasos mediante el comando docker run desde la línea de
comandos. Por lo tanto, cada servicio definido en el archivo docker-compose.yml debe
especificar exactamente una imagen o compilación. El resto de las claves son opcionales
y son análogas a sus equivalentes de la línea de comandos de docker run .
yml
version: '3.4'
services:
webmvc:
image: eshop/webmvc
environment:
- CatalogUrl=http://catalog-api
- OrderingUrl=http://ordering-api
- BasketUrl=http://basket-api
ports:
- "5100:80"
depends_on:
- catalog-api
- ordering-api
- basket-api
catalog-api:
image: eshop/catalog-api
environment:
- ConnectionString=Server=sqldata;Initial Catalog=CatalogData;User
Id=sa;Password=[PLACEHOLDER]
expose:
- "80"
ports:
- "5101:80"
#extra hosts can be used for standalone SQL Server or services at the
dev PC
extra_hosts:
- "CESARDLSURFBOOK:10.0.75.1"
depends_on:
- sqldata
ordering-api:
image: eshop/ordering-api
environment:
- ConnectionString=Server=sqldata;Database=Services.OrderingDb;User
Id=sa;Password=[PLACEHOLDER]
ports:
- "5102:80"
#extra hosts can be used for standalone SQL Server or services at the
dev PC
extra_hosts:
- "CESARDLSURFBOOK:10.0.75.1"
depends_on:
- sqldata
basket-api:
image: eshop/basket-api
environment:
- ConnectionString=sqldata
ports:
- "5103:80"
depends_on:
- sqldata
sqldata:
environment:
- SA_PASSWORD=[PLACEHOLDER]
- ACCEPT_EULA=Y
ports:
- "5434:1433"
basketdata:
image: redis
La clave raíz de este archivo es "services" (servicios). En esa clave se definen los servicios
que se quieren implementar y ejecutar al ejecutar el comando docker-compose up , o
bien al implementarlos desde Visual Studio mediante este archivo docker-compose.yml.
En este caso, el archivo docker-compose.yml tiene varios servicios definidos, como se
describe en la tabla siguiente.
webmvc Contenedor que incluye la aplicación ASP.NET Core MVC que consume los
microservicios de C# del lado servidor
catalog-api Contenedor que incluye el microservicio Catalog de la API web de ASP.NET Core
sqldata Contenedor que ejecuta SQL Server para Linux, que contiene las bases de datos
de microservicios
basket-api Contenedor que incluye el microservicio Basket de la API web de ASP.NET Core
basketdata Contenedor que ejecuta el servicio Redis Cache, con la base de datos Basket
como caché de Redis
Contenedor de la API de servicio web simple
Si nos centramos en un único contenedor, el microservicio de contenedor catalog-api
tiene una definición sencilla:
yml
catalog-api:
image: eshop/catalog-api
environment:
- ConnectionString=Server=sqldata;Initial Catalog=CatalogData;User
Id=sa;Password=[PLACEHOLDER]
expose:
- "80"
ports:
- "5101:80"
#extra hosts can be used for standalone SQL Server or services at the
dev PC
extra_hosts:
- "CESARDLSURFBOOK:10.0.75.1"
depends_on:
- sqldata
El nombre de SQL Server es sqldata, que es el mismo nombre que se usa para el
contenedor que ejecuta la instancia de SQL Server para Linux. Esto resulta práctico:
poder usar esta resolución de nombres (interna al host de Docker) resolverá la
dirección de red, por lo que no necesita saber la dirección IP interna de los
contenedores a los que tiene acceso desde otros contenedores.
Dado que la cadena de conexión se define mediante una variable de entorno, podría
establecer esa variable mediante otro mecanismo y en otro momento. Por ejemplo,
podría establecer una cadena de conexión diferente al efectuar una implementación en
producción en los hosts finales, o haciéndolo desde sus canalizaciones de CI/CD en
Azure DevOps Services o en su sistema de DevOps preferido.
Expone el puerto 80 para el acceso interno al servicio catalog-api dentro del host
de Docker. Actualmente, el host es una máquina virtual de Linux porque se basa en
una imagen de Docker para Linux, aunque podría configurar el contenedor para
que se ejecute en una imagen de Windows.
Reenvía el puerto 80 expuesto del contenedor al puerto 5101 del equipo host de
Docker (la máquina virtual de Linux).
También existen otras opciones más avanzadas de los archivos docker-compose.yml que
se exponen en las siguientes secciones.
Entornos de desarrollo
Entornos de prueba
Una parte importante de cualquier proceso de implementación continua (CD) o de
integración continua (CI) son las pruebas unitarias y las pruebas de integración. Estas
pruebas automatizadas requieren un entorno aislado, por lo que no se ven afectadas
por los usuarios ni por ningún otro cambio efectuado en los datos de la aplicación.
Con Docker Compose puede crear y destruir ese entorno aislado de un modo muy
sencillo ejecutando unos scripts o comandos en el símbolo del sistema, como los
comandos siguientes:
Consola
Implementaciones de producción
Si usa cualquier otro orquestador (Azure Service Fabric, Kubernetes, etc.), es posible que
tenga que agregar valores de configuración de instalación y metadatos como los de
docker-compose.yml, pero con el formato necesario para el otro orquestador.
Figura 6-12. Varios archivos docker-compose invalidan los valores del archivo base
docker-compose.yml
#docker-compose.yml (Base)
version: '3.4'
services:
basket-api:
image: eshop/basket-api:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Basket/Basket.API/Dockerfile
depends_on:
- basketdata
- identity-api
- rabbitmq
catalog-api:
image: eshop/catalog-api:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Catalog/Catalog.API/Dockerfile
depends_on:
- sqldata
- rabbitmq
marketing-api:
image: eshop/marketing-api:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Marketing/Marketing.API/Dockerfile
depends_on:
- sqldata
- nosqldata
- identity-api
- rabbitmq
webmvc:
image: eshop/webmvc:${TAG:-latest}
build:
context: .
dockerfile: src/Web/WebMVC/Dockerfile
depends_on:
- catalog-api
- ordering-api
- identity-api
- basket-api
- marketing-api
sqldata:
image: mcr.microsoft.com/mssql/server:2019-latest
nosqldata:
image: mongo
basketdata:
image: redis
rabbitmq:
image: rabbitmq:3-management
Los valores del archivo base docker-compose.yml no deberían variar porque haya
distintos entornos de implementación de destino.
Si se centra en la definición del servicio webmvc, por ejemplo, puede ver que esa
información es la misma con independencia del entorno que fije como objetivo.
Dispone de la siguiente información:
Dependencias de otros servicios, por lo que este contenedor no se inicia hasta que
se hayan iniciado los otros contenedores de dependencia.
Puede tener otra configuración, pero lo importante es que en el archivo base docker-
compose.yml solo establezca la información que es común en todos los entornos.
Luego, en el archivo docker-compose.override.yml o en archivos similares de
producción o almacenamiento provisional, debería colocar la configuración específica
para cada entorno.
yml
services:
# Simplified number of services here:
basket-api:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://0.0.0.0:80
- ConnectionString=${ESHOP_AZURE_REDIS_BASKET_DB:-basketdata}
- identityUrl=http://identity-api
- IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105
- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq}
- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME}
- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD}
- AzureServiceBusEnabled=False
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}
- OrchestratorType=${ORCHESTRATOR_TYPE}
- UseLoadTest=${USE_LOADTEST:-False}
ports:
- "5103:80"
catalog-api:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://0.0.0.0:80
- ConnectionString=${ESHOP_AZURE_CATALOG_DB:-
Server=sqldata;Database=Microsoft.eShopOnContainers.Services.CatalogDb;User
Id=sa;Password=[PLACEHOLDER]}
- PicBaseUrl=${ESHOP_AZURE_STORAGE_CATALOG_URL:-
http://host.docker.internal:5202/api/v1/catalog/items/[0]/pic/}
- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq}
- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME}
- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD}
- AzureStorageAccountName=${ESHOP_AZURE_STORAGE_CATALOG_NAME}
- AzureStorageAccountKey=${ESHOP_AZURE_STORAGE_CATALOG_KEY}
- UseCustomizationData=True
- AzureServiceBusEnabled=False
- AzureStorageEnabled=False
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}
- OrchestratorType=${ORCHESTRATOR_TYPE}
ports:
- "5101:80"
marketing-api:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://0.0.0.0:80
- ConnectionString=${ESHOP_AZURE_MARKETING_DB:-
Server=sqldata;Database=Microsoft.eShopOnContainers.Services.MarketingDb;Use
r Id=sa;Password=[PLACEHOLDER]}
- MongoConnectionString=${ESHOP_AZURE_COSMOSDB:-mongodb://nosqldata}
- MongoDatabase=MarketingDb
- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq}
- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME}
- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD}
- identityUrl=http://identity-api
- IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105
- CampaignDetailFunctionUri=${ESHOP_AZUREFUNC_CAMPAIGN_DETAILS_URI}
- PicBaseUrl=${ESHOP_AZURE_STORAGE_MARKETING_URL:-
http://host.docker.internal:5110/api/v1/campaigns/[0]/pic/}
- AzureStorageAccountName=${ESHOP_AZURE_STORAGE_MARKETING_NAME}
- AzureStorageAccountKey=${ESHOP_AZURE_STORAGE_MARKETING_KEY}
- AzureServiceBusEnabled=False
- AzureStorageEnabled=False
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}
- OrchestratorType=${ORCHESTRATOR_TYPE}
- UseLoadTest=${USE_LOADTEST:-False}
ports:
- "5110:80"
webmvc:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://0.0.0.0:80
- PurchaseUrl=http://webshoppingapigw
- IdentityUrl=http://10.0.75.1:5105
- MarketingUrl=http://webmarketingapigw
- CatalogUrlHC=http://catalog-api/hc
- OrderingUrlHC=http://ordering-api/hc
- IdentityUrlHC=http://identity-api/hc
- BasketUrlHC=http://basket-api/hc
- MarketingUrlHC=http://marketing-api/hc
- PaymentUrlHC=http://payment-api/hc
- SignalrHubUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5202
- UseCustomizationData=True
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}
- OrchestratorType=${ORCHESTRATOR_TYPE}
- UseLoadTest=${USE_LOADTEST:-False}
ports:
- "5100:80"
sqldata:
environment:
- SA_PASSWORD=[PLACEHOLDER]
- ACCEPT_EULA=Y
ports:
- "5433:1433"
nosqldata:
ports:
- "27017:27017"
basketdata:
ports:
- "6379:6379"
rabbitmq:
ports:
- "15672:15672"
- "5672:5672"
Para usar varios archivos de invalidación, o un archivo de invalidación con otro nombre,
puede usar la opción -f con el comando docker-compose y especificar los archivos. Cree
los archivos de combinaciones en el orden en que se especifican en la línea de
comandos. En el ejemplo siguiente se muestra cómo efectuar la implementación con
archivos de invalidación.
Consola
yml
IdentityUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105
sh
# .env file
ESHOP_EXTERNAL_DNS_NAME_OR_IP=host.docker.internal
ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP=10.121.122.92
Docker-compose espera que cada línea de los archivos .env tenga el formato
<variable>=<valor>.
Recursos adicionales
Introducción a Docker Compose
https://docs.docker.com/compose/overview/
Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:7.0
WORKDIR /app
ENV ASPNETCORE_URLS http://+:80
EXPOSE 80
COPY . .
RUN dotnet restore
ENTRYPOINT ["dotnet", "run"]
Un Dockerfile como este funcionará, pero puede optimizar considerablemente sus
imágenes, sobre todo las imágenes de producción.
El equipo de .NET ha estado trabajando mucho para convertir .NET y ASP.NET Core en
un marco optimizado para contenedores. .NET no solo es un marco ligero con una
superficie de memoria pequeña; el equipo se ha centrado en imágenes de Docker
optimizadas para los tres escenarios principales y las ha publicado en el registro de
Docker Hub en dotnet/ , empezando por la versión 2.1:
El equipo de .NET proporciona cuatro variantes básicas en dotnet (en Docker Hub):
Para un inicio más rápido, las imágenes en tiempo de ejecución también configuran
automáticamente las direcciones aspnetcore_url en el puerto 80 y usan Ngen para crear
una caché de imágenes nativa de ensamblados.
Recursos adicionales
Compilación de imágenes de Docker optimizadas con ASP.NET
Corehttps://learn.microsoft.com/archive/blogs/stevelasker/building-optimized-
docker-images-with-asp-net-core
Anterior Siguiente
Uso de un servidor de bases de datos
que se ejecuta como contenedor
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Puede tener las bases de datos (SQL Server, PostgreSQL, MySQL, etc.) en servidores
independientes regulares, en clústeres locales o en los servicios PaaS en la nube como
Azure SQL DB. Sin embargo, en los entornos de desarrollo y prueba, el hecho de que las
bases de datos se ejecuten como contenedores resulta práctico, ya que no tiene
ninguna dependencia externa y solo con ejecutar el comando docker-compose up ya se
inicia toda la aplicación. Tener esas bases de datos como contenedores también es muy
útil para las pruebas de integración, porque la base de datos se inicia en el contenedor y
siempre se rellena con los mismos datos de ejemplo, por lo que las pruebas pueden ser
más predecibles.
Un punto clave de los microservicios es que cada uno posee sus datos relacionados, de
manera que debería tener su propia base de datos. Sin embargo, las bases de datos
pueden encontrarse en cualquier lugar. En este caso, todas están en el mismo
contenedor para mantener los requisitos de memoria de Docker tan bajos como sea
posible. Tenga en cuenta que esta es una solución suficientemente aceptable para el
desarrollo y tal vez para las pruebas, pero no para la producción.
yml
sqldata:
image: mcr.microsoft.com/mssql/server:2017-latest
environment:
- SA_PASSWORD=Pass@word
- ACCEPT_EULA=Y
ports:
- "5434:1433"
PowerShell
Cuando se inicia este contenedor de SQL Server por primera vez, el contenedor inicializa
SQL Server con la contraseña que usted proporcione. Una vez que SQL Server se ejecuta
como un contenedor, puede actualizar la base de datos mediante la conexión a través
de cualquier conexión de SQL habitual, como en SQL Server Management Studio, Visual
Studio o código C#.
La aplicación eShopOnContainers inicializa cada base de datos de microservicio con
datos de ejemplo que propaga al inicio, tal como se describe en la sección siguiente.
El hecho de que SQL Server se ejecute como un contenedor no solo es útil para una
demo, donde puede que no tenga acceso a una instancia de SQL Server, sino que
también es perfecto para entornos de desarrollo y prueba, de manera que puede
realizar fácilmente pruebas de integración a partir de una imagen limpia de SQL Server y
datos conocidos propagando nuevos datos de ejemplo.
Recursos adicionales
Ejecución de imágenes de Docker de SQL Server en Linux, Mac o Windows
https://learn.microsoft.com/sql/linux/sql-server-linux-setup-docker
C#
Log.Logger = CreateSerilogLogger(configuration);
try
{
Log.Information("Configuring web host ({ApplicationContext})...",
AppName);
var host = CreateHostBuilder(configuration, args);
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Program terminated unexpectedly
({ApplicationContext})!", AppName);
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
Existe una salvedad importante a la hora de aplicar migraciones e inicializar una base de
datos durante el inicio de un contenedor. Dado que es posible que la base de datos no
esté disponible por algún motivo, debe controlar los reintentos mientras espera a que el
servidor esté disponible. El método de extensión MigrateDbContext() controla esta
lógica de reintentos, como se muestra en el código siguiente:
C#
try
{
logger.LogInformation("Migrating database associated with
context {DbContextName}", typeof(TContext).Name);
if (underK8s)
{
InvokeSeeder(seeder, context, services);
}
else
{
var retry = Policy.Handle<SqlException>()
.WaitAndRetry(new TimeSpan[]
{
TimeSpan.FromSeconds(3),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(8),
});
return host;
}
C#
Al realizar pruebas de integración, resulta útil disponer de una forma de generar datos
coherentes con las pruebas de integración. Poder crear cualquier cosa de cero, incluida
una instancia de SQL Server que se ejecuta en un contenedor, es muy útil para los
entornos de prueba.
C#
Sin embargo, hay un truco importante. La base de datos en memoria no admite muchas
restricciones que son específicas de una base de datos determinada. Por ejemplo,
podría agregar un índice único en una columna en el modelo de EF Core y escribir una
prueba en la base de datos en memoria para comprobar que no le permite agregar un
valor duplicado. Pero cuando usa la base de datos en memoria, no puede controlar los
índices únicos en una columna. Por tanto, la base de datos en memoria no se comporta
exactamente igual que una base de datos de SQL Server real: no emula restricciones
específicas de la base de datos.
Aun así, una base de datos en memoria también es útil para las pruebas y la creación de
prototipos. Pero si quiere crear pruebas de integración precisas que tengan en cuenta el
comportamiento de la implementación de una base de datos determinada, debe usar
una base de datos real como SQL Server. Para ello, ejecutar SQL Server en un
contenedor es una gran opción, más precisa que el proveedor de base de datos de EF
Core InMemory.
Sin embargo, al ejecutar Redis en producción, es mejor buscar una solución de alta
disponibilidad como Redis Microsoft Azure, que se ejecuta como una PaaS (plataforma
como servicio). En el código, solo debe cambiar las cadenas de conexión.
Redis proporciona una imagen de Docker con Redis. Esa imagen está disponible en
Docker Hub en esta dirección URL:
https://hub.docker.com/_/redis/
Consola
La imagen de Redis incluye expose:6379 (el puerto que usa Redis), de manera que la
vinculación de contenedor estándar hará que esté automáticamente disponible para los
contenedores vinculados.
yml
#docker-compose.yml file
#...
basketdata:
image: redis
expose:
- "6379"
yml
basket-api:
environment:
# Other data ...
- ConnectionString=basketdata
- EventBusConnection=rabbitmq
Anterior Siguiente
Implementación de comunicación
basada en eventos entre microservicios
(eventos de integración)
Artículo • 29/03/2023
Sugerencia
Descargar PDF
Puede usar eventos para implementar transacciones de negocio que abarquen varios
servicios, lo cual termina proporcionando coherencia entre dichos servicios. Una
eventual transacción coherente consta de una serie de acciones distribuidas. En cada
acción, el microservicio actualiza una entidad de negocio y publica un evento que
desencadena la siguiente acción. En la figura 6-18 siguiente se muestra un evento
PriceUpdated publicado mediante un bus de eventos, para que la actualización de los
precios se propague a la cesta y a otros microservicios.
En esta sección se describe cómo puede implementar este tipo de comunicación con
.NET mediante un bus de eventos genéricos, como se muestra en la Figura 6-18. Hay
varias implementaciones posibles, cada una de las cuales usa una tecnología o
infraestructura distinta, como RabbitMQ, Azure Service Bus o cualquier otro bus de
servicio de código abierto de terceros o comercial.
Para implementar solo una prueba de concepto de bus de eventos para su entorno de
desarrollo, como en el ejemplo de eShopOnContainers, una implementación sencilla
encima de RabbitMQ ejecutándose como contenedor podría ser suficiente. Pero para
sistemas importantes y de producción que necesiten alta escalabilidad, se recomienda
evaluar y usar Azure Service Bus.
Obviamente, siempre puede crear sus propias características de bus de servicio sobre
tecnologías de nivel inferior, como RabbitMQ y Docker, pero el trabajo necesario para
"volver a inventar la rueda" puede ser demasiado costoso para una aplicación de
empresa personalizada.
Eventos de integración
Los eventos de integración se utilizan para sincronizar el estado de dominio en varios
microservicios o sistemas externos. Esta funcionalidad se lleva a cabo mediante la
publicación de eventos de integración fuera del microservicio. Cuando se publica un
evento en varios microservicios de receptor (en tantos microservicios como estén
suscritos al evento de integración), el controlador de eventos correspondiente en cada
microservicio de receptor controla el evento.
C#
Solo hay unos cuantos tipos de bibliotecas que debería compartir entre microservicios.
Por un lado, las bibliotecas que son bloques de aplicaciones finales, como la API de
cliente de bus de eventos , como en eShopOnContainers. Por otro lado, las bibliotecas
que constituyen herramientas que también se podrían compartir como componentes de
NuGet, igual que los serializadores JSON.
El bus de eventos
Un bus de eventos permite una comunicación de estilo de suscripción/publicación entre
microservicios, sin requerir que los componentes se reconozcan entre sí, como se
muestra en la Figura 6-19.
Figura 6-19. Aspectos básicos de publicación/suscripción con un bus de eventos
Patrón de observador
En el patrón de observador , su objeto principal (conocido como "Observable")
proporciona información pertinente (eventos) a otros objetos interesados (conocidos
como "Observadores").
La abstracción o interfaz.
En la Figura 6-19 puede ver cómo, desde el punto de vista de la aplicación, el bus de
eventos no es más que un canal de Pub/Sus. La forma de implementar este tipo de
comunicación asincrónica puede variar. Puede tener varias implementaciones para
intercambiarlas dependiendo de los requisitos del entorno (por ejemplo, entornos de
producción frente a entornos de desarrollo).
En la figura 6-20, puede ver una abstracción de un bus de eventos con varias
implementaciones basadas en tecnologías de mensajería de infraestructura, como
RabbitMQ, Azure Service Bus, u otro agente de eventos o de mensajería.
Es conveniente definir el bus de eventos a través de una interfaz de forma que pueda
implementarse con varias tecnologías como RabbitMQ y Azure Service Bus, entre otras.
Pero, como se ha mencionado anteriormente, usar sus propias abstracciones (la interfaz
del bus de eventos) solo es una buena opción si necesita características de bus de
eventos compatibles con sus abstracciones. Si necesita características más completas del
bus de servicio, probablemente tendrá que usar la API y las abstracciones
proporcionadas por el bus de servicio comercial que prefiera, en vez de usar sus propias
abstracciones.
C#
Los microservicios que quieren recibir eventos utilizan los métodos Subscribe (puede
tener varias implementaciones dependiendo de los argumentos). Este método tiene dos
argumentos. El primero es el evento de integración para suscribirse a
( IntegrationEvent ). El segundo es el controlador del evento de integración (o el método
de devolución de llamada), denominado IIntegrationEventHandler<T> , que se ejecuta
cuando el microservicio receptor recibe ese mensaje de evento de integración.
Recursos adicionales
Algunas soluciones de mensajería listas para producción:
NServiceBus
https://particular.net/nservicebus
MassTransit
https://masstransit-project.com/
Anterior Siguiente
Implementación de un bus de eventos
con RabbitMQ para el entorno de
desarrollo o de prueba
Artículo • 09/05/2023
Sugerencia
Descargar PDF
Para empezar, hay que decir que si crea el bus de eventos personalizado basado en
RabbitMQ que se ejecuta en un contenedor, como hace la aplicación
eShopOnContainers, debería usarse únicamente para los entornos de desarrollo y
prueba. No lo use para el entorno de producción, a menos que lo cree como parte de
un bus de servicio listo para producción, tal como se describe en la sección Recursos
adicionales que aparece a continuación. En un bus de eventos personalizado simple
pueden faltar muchas de las características críticas para entornos de producción que
tiene un bus de servicio comercial.
La implementación del bus de eventos con RabbitMQ permite que los microservicios se
suscriban a eventos, los publiquen y los reciban, tal como se muestra en la figura 6-21.
Figura 6-21. Implementación de RabbitMQ de un bus de eventos
C#
C#
_subsManager.AddSubscription<T, TH>();
}
}
Cada tipo de evento tiene un canal relacionado para obtener eventos de RabbitMQ.
Puede tener tantos controladores de eventos por tipo de canal y evento como sea
necesario.
Recursos adicionales
Solución lista para la producción compatibles con RabbitMQ.
Anterior Siguiente
Suscripción a eventos
Artículo • 29/03/2023
Sugerencia
Descargar PDF
El primer paso para usar el bus de eventos es suscribir los microservicios a los eventos
que quieren recibir. Esa funcionalidad debe realizarse en los microservicios del receptor.
C#
eventBus.Subscribe<ProductPriceChangedIntegrationEvent,
ProductPriceChangedIntegrationEventHandler>();
eventBus.Subscribe<OrderStartedIntegrationEvent,
OrderStartedIntegrationEventHandler>();
Después de ejecutar este código, el microservicio de suscriptor escuchará a través de los
canales de RabbitMQ. Cuando llega algún mensaje de tipo
ProductPriceChangedIntegrationEvent, el código invoca el controlador de eventos que
se le pasa y procesa el evento.
C#
[Route("api/v1/[controller]")]
public class CatalogController : ControllerBase
{
private readonly CatalogContext _context;
private readonly IOptionsSnapshot<Settings> _settings;
private readonly IEventBus _eventBus;
Después, se usa desde los métodos del dispositivo, como en el método UpdateProduct:
C#
[Route("items")]
[HttpPost]
public async Task<IActionResult> UpdateProduct([FromBody]CatalogItem
product)
{
var item = await _context.CatalogItems.SingleOrDefaultAsync(
i => i.Id == product.Id);
// ...
if (item.Price != product.Price)
{
var oldPrice = item.Price;
item.Price = product.Price;
_context.CatalogItems.Update(item);
var @event = new ProductPriceChangedIntegrationEvent(item.Id,
item.Price,
oldPrice);
// Commit changes in original transaction
await _context.SaveChangesAsync();
// Publish integration event to the event bus
// (RabbitMQ or a service bus underneath)
_eventBus.Publish(@event);
// ...
}
// ...
}
Uso del patrón de bandeja de salida . Se trata de una tabla transaccional para
almacenar los eventos de integración (extendiendo la transacción local).
En este escenario, el uso del modelo de orígenes de evento (ES) completo es uno de los
mejores métodos, si no el mejor. Pero en muchas situaciones, es posible que no pueda
implementar un sistema de ES completo. Con los orígenes de evento solo se almacenan
los eventos de dominio en la base de datos transaccional, en lugar de almacenar los
datos de estado actuales. Almacenar solo los eventos de dominio puede tener grandes
ventajas, como tener el historial del sistema disponible y poder determinar el estado del
sistema en cualquier momento del pasado. Pero la implementación de un sistema de ES
completo requiere que se cambie la arquitectura de la mayor parte del sistema y
presenta otras muchas complejidades y requisitos. Por ejemplo, le interesaría usar una
base de datos creada específicamente para los orígenes de eventos, como Event
Store , o bien una base de datos orientada a documentos como Azure Cosmos DB,
MongoDB, Cassandra, CouchDB o RavenDB. Los orígenes de evento son un buen
enfoque para este problema, pero no es la solución más sencilla a menos que ya esté
familiarizado con los orígenes de eventos.
La opción de usar la minería del registro de transacciones parece transparente en un
principio. Pero para usar este enfoque, el microservicio debe acoplarse al registro de
transacciones de RDBMS, como el registro de transacciones de SQL Server.
Probablemente, este enfoque no sea deseable. Otra desventaja es que es posible que
las actualizaciones de bajo nivel en el registro de transacciones no estén al mismo nivel
que los eventos de integración generales. En ese caso, el proceso de utilización de
técnicas de ingeniería inversa en esas operaciones de registro de transacciones puede
ser complicado.
Tenga en cuenta que, con este enfoque, solo se conservan los eventos de integración
para cada microservicio de origen y solo los eventos que le interesa comunicar con
otros microservicios o sistemas externos. Por el contrario, en un sistema de ES completo,
también se almacenan todos los eventos de dominio.
Por tanto, este enfoque equilibrado es un sistema de ES simplificado. Necesita una lista
de eventos de integración con su estado actual ("listo para publicar" frente a
"publicado"). Pero solo tiene que implementar estos estados para los eventos de
integración. Y en este enfoque, no tendrá que almacenar todos los datos de dominio
como eventos en la base de datos transaccional, tal y como haría en un sistema de ES
completo.
Si ya usa una base de datos relacional, puede usar una tabla transaccional para
almacenar los eventos de integración. Para lograr la atomicidad en la aplicación, se usa
un proceso de dos pasos basado en transacciones locales. Básicamente, dispone de una
tabla IntegrationEvent en la misma base de datos donde se encuentren las entidades de
dominio. Esa tabla funciona como un seguro para lograr la atomicidad, de modo que los
eventos de integración guardados se incluyan en las mismas transacciones con las que
se confirman los datos de dominio.
Al implementar los pasos necesarios para publicar los eventos, dispone de las opciones
siguientes:
Sobre el segundo enfoque: se usa la tabla EventLog como una cola y siempre se usa un
microservicio de trabajo para publicar los mensajes. En ese caso, el proceso es similar al
que se muestra en la figura 6-23. Esto muestra un microservicio adicional y la tabla es el
único origen cuando se publican los eventos.
C#
productToUpdate.Id);
if (catalogItem == null) return NotFound();
if (catalogItem.Price != productToUpdate.Price)
raiseProductPriceChangedEvent = true;
productToUpdate.Price,
oldPrice);
}
// Update current product
catalogItem = productToUpdate;
// Just save the updated product if the Product's Price hasn't changed.
if (!raiseProductPriceChangedEvent)
{
await _catalogContext.SaveChangesAsync();
}
else // Publish to event bus only if product price changed
{
// Achieving atomicity between original DB and the
IntegrationEventLog
// with a local transaction
using (var transaction =
_catalogContext.Database.BeginTransaction())
{
_catalogContext.CatalogItems.Update(catalogItem);
await _catalogContext.SaveChangesAsync();
await
_integrationEventLogService.SaveEventAsync(priceChangedEvent);
transaction.Commit();
}
_integrationEventLogService.MarkEventAsPublishedAsync(
priceChangedEvent);
}
return Ok();
}
Un controlador de eventos recibe por primera vez una instancia de evento desde el bus
de eventos. Después, busca el componente que se va a procesar relacionado con ese
evento de integración y lo propaga y conserva como un cambio de estado en el
microservicio de receptor. Por ejemplo, si un evento ProductPriceChanged se origina en
el microservicio de catálogo, se controla en el microservicio de cesta de la compra y
también cambia el estado en este microservicio de receptor, como se muestra en el
código siguiente.
C#
namespace
Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandl
ing
{
public class ProductPriceChangedIntegrationEventHandler :
IIntegrationEventHandler<ProductPriceChangedIntegrationEvent>
{
private readonly IBasketRepository _repository;
public ProductPriceChangedIntegrationEventHandler(
IBasketRepository repository)
{
_repository = repository;
}
Un ejemplo de una operación idempotente es una instrucción SQL que inserta datos en
una tabla solo si esos datos no están ya en la tabla. No importa cuántas veces se ejecute
esa instrucción SQL de inserción; el resultado será el mismo: la tabla contendrá esos
datos. Este tipo de idempotencia también puede ser necesaria cuando se trabaja con
mensajes si existe la posibilidad de que se envíen y, por tanto, se procesen más de una
vez. Por ejemplo, si la lógica de reintento hace que un remitente envíe exactamente el
mismo mensaje más de una vez, tendrá que asegurarse de que sea idempotente.
Se pueden diseñar mensajes idempotentes. Por ejemplo, puede crear un evento que
diga «establecer un precio de 25 paraelproducto», envezde«añadir5 al precio del
producto». Podría procesar de forma segura el mensaje tantas veces como quiera y se
produciría el mismo resultado. Esto no es cierto para el segundo mensaje. Pero incluso
en el primer caso, es posible que no le interese procesar el primer evento, porque el
sistema también podría haber enviado un evento de cambio de precio más reciente y se
podría sobrescribir el precio de nuevo.
Otro ejemplo podría ser un evento de pedido completado que se propaga a varios
suscriptores. La aplicación tiene que asegurarse de que la información del pedido se
actualice una sola vez en otros sistemas, aunque haya eventos de mensaje duplicados
para el mismo evento de pedido completado.
Es conveniente tener algún tipo de identidad por evento para poder crear lógica que
exija que cada evento se procese solo una vez por cada receptor.
Recursos adicionales
Respeto de la idempotencia de los mensajes
https://learn.microsoft.com/previous-versions/msp-n-
p/jj591565(v=pandp.10)#honoring-message-idempotency
Recursos adicionales
Bifurcación de eShopOnContainers mediante NServiceBus [Particular Software]
https://go.particular.net/eShopOnContainers
Teorema CAP
https://en.wikipedia.org/wiki/CAP_theorem
Rick Saling. The CAP Theorem: Why “Everything is Different” with the Cloud and
Internet (Teorema CAP: por qué "todo es diferente" con la nube e Internet)
https://learn.microsoft.com/archive/blogs/rickatmicrosoft/the-cap-theorem-why-
everything-is-different-with-the-cloud-and-internet/
Eric Brewer. CAP Twelve Years Later: How the "Rules" Have Changed (CAP 12
años después: cómo han cambiado las "reglas")
https://www.infoq.com/articles/cap-twelve-years-later-how-the-rules-have-
changed
Anterior Siguiente
Probar aplicaciones web y servicios
ASP.NET Core
Artículo • 24/03/2023
Sugerencia
Descargar PDF
Los controladores son una parte fundamental de cualquier servicio de la API de ASP.NET
Core y de la aplicación web de ASP.NET MVC. Por lo tanto, debe tener la seguridad de
que se comportan según lo previsto en la aplicación. Las pruebas automatizadas pueden
darle esta seguridad, así como detectar errores antes de que lleguen a la fase
producción.
Debe probar cómo se comporta el controlador según las entradas válidas o no válidas y
probar las respuestas del controlador en función del resultado de la operación comercial
que lleve a cabo. Pero debe realizar estos tipos de pruebas en los microservicios:
Pruebas de servicio. Estas pruebas garantizan que se pongan a prueba todos los
casos de uso de servicio de un extremo a otro, incluidas pruebas de servicios
múltiples al mismo tiempo. Para este tipo de prueba, primero debe preparar el
entorno. En este caso, esto significa iniciar los servicios (por ejemplo, mediante el
uso de Docker Compose).
Al escribir una prueba unitaria para un controlador de API web, puede ejemplificar
directamente la clase de controlador mediante la nueva palabra clave en C#, para que la
prueba se ejecute tan rápido como sea posible. En el ejemplo siguiente se muestra
cómo hacerlo con XUnit como marco de pruebas.
C#
[Fact]
public async Task Get_order_detail_success()
{
//Arrange
var fakeOrderId = "12";
var fakeOrder = GetFakeOrder();
//...
//Act
var orderController = new OrderController(
_orderServiceMock.Object,
_basketServiceMock.Object,
_identityParserMock.Object);
orderController.ControllerContext.HttpContext = _contextMock.Object;
var actionResult = await orderController.Detail(fakeOrderId);
//Assert
var viewResult = Assert.IsType<ViewResult>(actionResult);
Assert.IsAssignableFrom<Order>(viewResult.ViewData.Model);
}
Como las pruebas de integración usan segmentos de código más grandes que las
pruebas unitarias y dependen de los elementos de infraestructura, tienden a ser órdenes
de envergadura, más lentas que las pruebas unitarias. Por lo tanto, es conveniente
limitar el número de pruebas de integración que va a escribir y a ejecutar.
ASP.NET Core incluye un host web de prueba integrado que puede usarse para
controlar las solicitudes HTTP sin causar una sobrecarga en la red, lo que significa que
puede ejecutar dichas pruebas más rápidamente si usa un host de web real. El host web
de prueba (TestServer) está disponible en un componente NuGet como
Microsoft.AspNetCore.TestHost. Se puede agregar a proyectos de prueba de integración
y utilizarlo para hospedar aplicaciones de ASP.NET Core.
C#
public PrimeWebDefaultRequestShould()
{
// Arrange
_server = new TestServer(new WebHostBuilder()
.UseStartup<Startup>());
_client = _server.CreateClient();
}
[Fact]
public async Task ReturnHelloWorld()
{
// Act
var response = await _client.GetAsync("/");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal("Hello World!", responseString);
}
}
Recursos adicionales
docker-compose-test.yml
yml
version: '3.4'
services:
redis.data:
image: redis:alpine
rabbitmq:
image: rabbitmq:3-management-alpine
sqldata:
image: mcr.microsoft.com/mssql/server:2017-latest
nosqldata:
image: mongo
docker-compose-test.override.yml
yml
version: '3.4'
services:
redis.data:
ports:
- "6379:6379"
rabbitmq:
ports:
- "15672:15672"
- "5672:5672"
sqldata:
environment:
- SA_PASSWORD=Pass@word
- ACCEPT_EULA=Y
ports:
- "5433:1433"
nosqldata:
ports:
- "27017:27017"
Por tanto, para ejecutar las pruebas de integración y funcionales primero debe ejecutar
este comando, desde la carpeta de prueba de la solución:
Consola
Como puede ver, estos archivos docker-compose solo inician los microservicios Redis,
RabbitMQ, SQL Server y MongoDB.
Recursos adicionales
Pruebas unitarias y de integración en eShopOnContainers
https://github.com/dotnet-architecture/eShopOnContainers/wiki/Unit-and-
integration-testing
Anterior Siguiente
Implementar tareas en segundo plano
en microservicios con IHostedService y
la clase BackgroundService
Artículo • 29/03/2023
Sugerencia
Descargar PDF
Las tareas en segundo plano y los trabajos programados son elementos que podría
necesitar para cualquier aplicación, tanto si esta sigue el patrón de arquitectura de
microservicios como si no. La diferencia al usar una arquitectura de microservicios es
que se puede implementar la tarea en segundo plano en un proceso o contenedor
independiente para el hospedaje, de modo que se pueda modificar su escala en función
de las necesidades.
Desde un punto de vista genérico, en .NET este tipo de tareas se llaman Servicios
hospedados, puesto que son servicios o lógica que se hospedan en el host, la aplicación
o el microservicio. Observe que, en este caso, el servicio hospedado simplemente
significa una clase con la lógica de la tarea de segundo plano.
Desde la versión 2.0 de .NET Core, el marco proporciona una nueva interfaz denominada
IHostedService que le ayuda a implementar fácilmente servicios hospedados. La idea
básica es que pueda registrar varias tareas en segundo plano (servicios hospedados),
que se ejecutan en segundo plano mientras se ejecuta el host o host web, tal como se
muestra en la imagen 6-26.
Figura 6-26. Uso de IHostedService en un WebHost frente a un Host
ASP.NET Core 1.x y 2.x admiten IWebHost para los procesos en segundo plano en
aplicaciones web. .NET Core 2.1 y las versiones posteriores admiten IHost para los
procesos en segundo plano con aplicaciones de consola planas. Observe la diferencia
entre WebHost y Host .
WebHost (clase base que implementa IWebHost ) en ASP.NET Core 2.0 es el artefacto de
Un Host (clase base que implementa IHost ) se presentó en .NET Core 2.1. Básicamente,
Host permite disponer de una infraestructura similar a la que se tiene con WebHost
(inserción de dependencias, servicios hospedados, etc.), pero en este caso tan solo
quiere tener un proceso sencillo y más ligero como host, sin ninguna relación con las
características de servidor HTTP, MVC o API Web.
Por lo tanto, puede elegir y crear un proceso de host especializado con IHost para
controlar los servicios hospedados y nada más, como por ejemplo un microservicio
hecho solo para hospedar IHostedServices , o bien ampliar un elemento WebHost de
ASP.NET Core existente, como una aplicación MVC o API web de ASP.NET Core.
Cada enfoque tiene ventajas e inconvenientes dependiendo de sus necesidades
empresariales y de escalabilidad. La conclusión es básicamente que, si las tareas en
segundo plano no tienen nada que ver con HTTP ( IWebHost ), debe usar IHost .
Una tarea en segundo plano que sondea una base de datos en busca de cambios.
Una tarea programada que actualiza una caché periódicamente.
Una implementación de QueueBackgroundWorkItem que permite que una tarea se
ejecute en un subproceso en segundo plano.
Procesar los mensajes de una cola de mensajes en el segundo plano de una
aplicación web mientras se comparten servicios comunes como ILogger .
Una tarea en segundo plano iniciada con Task.Run() .
C#
//Other DI registrations;
Interfaz de IHostedService
Al registrar un servicio IHostedService , .NET llamará a los métodos StartAsync() y
StopAsync() de su tipo IHostedService durante el inicio y la detención de la aplicación,
Pero como la mayoría de las tareas en segundo plano tienen necesidades similares en
relación con la administración de tokens de cancelación y otras operaciones habituales,
hay una clase base abstracta práctica denominada BackgroundService de la que puede
derivar (disponible desde .NET Core 2.1).
Esta clase proporciona el trabajo principal necesario para configurar la tarea en segundo
plano.
El código siguiente es la clase base abstracta BackgroundService tal y como se
implementa en .NET.
C#
// Copyright (c) .NET Foundation. Licensed under the Apache License, Version
2.0.
/// <summary>
/// Base class for implementing a long running <see cref="IHostedService"/>.
/// </summary>
public abstract class BackgroundService : IHostedService, IDisposable
{
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts =
new
CancellationTokenSource();
try
{
// Signal cancellation to the executing method
_stoppingCts.Cancel();
}
finally
{
// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite,
cancellationToken));
}
C#
public GracePeriodManagerService(IOptions<OrderingBackgroundSettings>
settings,
IEventBus eventBus,
ILogger<GracePeriodManagerService>
logger)
{
// Constructor's parameters validations...
}
stoppingToken.Register(() =>
_logger.LogDebug($" GracePeriod background task is stopping."));
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogDebug($"GracePeriod task doing background work.");
try {
await Task.Delay(_settings.CheckUpdateTime,
stoppingToken);
}
catch (TaskCanceledException exception) {
_logger.LogCritical(exception, "TaskCanceledException
Error", exception.Message);
}
}
.../...
}
Por supuesto, en su lugar puede ejecutar cualquier otra tarea en segundo plano
empresarial.
C#
WebHost.CreateDefaultBuilder(args)
.UseShutdownTimeout(TimeSpan.FromSeconds(10))
...
Diagrama de clases: IWebHost y IHost pueden hospedar muchos servicios, que heredan
de BackgroundService, que implementa IHostedService.
Anterior Siguiente
Implementación de puertas de enlace
de API con Ocelot
Artículo • 29/03/2023
Sugerencia
Descargar PDF
) Importante
Como también se puede observar en el diagrama, tener varias puertas de enlace de API
permite que varios equipos de desarrollo sean autónomos (en este caso, las
características de Marketing frente a las de Shopping) al desarrollar e implementar sus
microservicios además de sus propias puertas de enlace de API relacionadas.
Si tuviera una sola puerta de enlace de API monolítica, sería un único punto para
actualizar por varios equipos de desarrollo, que podrían acoplar todos los microservicios
con un único elemento de la aplicación.
Ampliando mucho más el diseño, en ocasiones una puerta de enlace de API específica
también se puede limitar a un microservicio empresarial individual en función de la
arquitectura elegida. El tener los límites de la puerta de enlace de API dictados por el
negocio o el dominio ayuda a lograr un mejor diseño.
Por ejemplo, la especificidad del nivel de puerta de enlace de API puede ser
especialmente útil para aplicaciones de interfaz de usuario compuesta más avanzadas
que se basan en microservicios, dado que el concepto de una puerta de enlace de API
específica es similar a un servicio de composición de interfaz de usuario.
Como punto clave, para muchas aplicaciones de tamaño medio y grande, el uso de una
puerta de enlace de API personalizada suele ser un enfoque adecuado, pero no como
un único agregador monolítico ni una única puerta de enlace de API personalizada
central, a menos que esa puerta de enlace de API permita varias áreas de configuración
independientes para que los diferentes equipos de desarrollo creen microservicios
autónomos.
Puede ver que el microservicio Catalog es un proyecto de API web de ASP.NET Core
típico con varios controladores y métodos, como en el código siguiente.
C#
[HttpGet]
[Route("items/{id:int}")]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType(typeof(CatalogItem),(int)HttpStatusCode.OK)]
public async Task<IActionResult> GetItemById(int id)
{
if (id <= 0)
{
return BadRequest();
}
var item = await _catalogContext.CatalogItems.
SingleOrDefaultAsync(ci => ci.Id
== id);
//…
if (item != null)
{
return Ok(item);
}
return NotFound();
}
La solicitud HTTP terminará ejecutando ese tipo de código de C# que accede a la base
de datos de microservicios además de cualquier otra acción requerida.
Dockerfile
El puerto 80 que se muestra en el código es interno dentro del host de Docker, por lo
que las aplicaciones cliente no pueden acceder a él.
Las aplicaciones cliente solo pueden acceder a los puertos externos (si existen)
publicados al implementar con docker-compose .
yml
catalog-api:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://0.0.0.0:80
- ConnectionString=YOUR_VALUE
- ... Other Environment Variables
ports:
- "5101:80" # Important: In a production environment you should remove
the external port (5101) kept here for microservice debugging purposes.
# The API Gateway redirects and access through the
internal port (80).
Consola
Este comando solo ejecuta el contenedor del servicio catalog-api, además de las
dependencias que se especifican en el archivo docker-compose.yml. En este caso, el
contenedor de SQL Server y el contenedor de RabbitMQ.
Pero la comunicación de acceso directo al microservicio, en este caso a través del puerto
externo 5101, es precisamente lo que se quiere evitar en la aplicación. Y se puede evitar
si se establece el nivel adicional de direccionamiento indirecto de la puerta de enlace de
API (en este caso, Ocelot). De ese modo, la aplicación cliente no accede directamente al
microservicio.
PowerShell
Install-Package Ocelot
Este proyecto ASP.NET Core WebHost se compila con dos archivos simples: Program.cs
y Startup.cs .
C#
namespace OcelotApiGw
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
{
var builder = WebHost.CreateDefaultBuilder(args);
"configuration.json")))
.UseStartup<Startup>();
var host = builder.Build();
return host;
}
}
}
enlace de API, es decir, los puntos de conexión externos con puertos específicos y los
puntos de conexión internos correlacionados, que normalmente usan otros puertos.
JSON
{
"ReRoutes": [],
"GlobalConfiguration": {}
}
JSON
{
"ReRoutes": [
{
"DownstreamPathTemplate": "/api/{version}/{everything}",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "catalog-api",
"Port": 80
}
],
"UpstreamPathTemplate": "/api/{version}/c/{everything}",
"UpstreamHttpMethod": [ "POST", "PUT", "GET" ]
},
{
"DownstreamPathTemplate": "/api/{version}/{everything}",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "basket-api",
"Port": 80
}
],
"UpstreamPathTemplate": "/api/{version}/b/{everything}",
"UpstreamHttpMethod": [ "POST", "PUT", "GET" ],
"AuthenticationOptions": {
"AuthenticationProviderKey": "IdentityApiKey",
"AllowedScopes": []
}
}
],
"GlobalConfiguration": {
"RequestIdKey": "OcRequestId",
"AdministrationPath": "/administration"
}
}
Por ejemplo, vamos a centrarnos en uno de los elementos ReRoute del archivo
configuration.json anterior, la configuración del microservicio Basket.
JSON
{
"DownstreamPathTemplate": "/api/{version}/{everything}",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "basket-api",
"Port": 80
}
],
"UpstreamPathTemplate": "/api/{version}/b/{everything}",
"UpstreamHttpMethod": [ "POST", "PUT", "GET" ],
"AuthenticationOptions": {
"AuthenticationProviderKey": "IdentityApiKey",
"AllowedScopes": []
}
}
que se use. Cuando se usa docker-compose, los nombres de los servicios los
proporciona el host de Docker, que usa los nombres de servicio proporcionados en los
archivos docker-compose. Si se usa un orquestador como Kubernetes o Service Fabric,
ese nombre se debe resolver mediante DNS o la resolución de nombres proporcionada
por cada orquestador.
En este momento, podría tener una única puerta de enlace de API de Ocelot (ASP.NET
Core WebHost) con uno o varios archivos configuration.json combinados , o bien
almacenar la configuración en un almacén de Consul KV .
Figura 6-33. Volver a usar una única imagen de Docker de Ocelot entre varios tipos de
puerta de enlace de API
mobileshoppingapigw:
image: eshop/ocelotapigw:${TAG:-latest}
build:
context: .
dockerfile: src/ApiGateways/ApiGw-Base/Dockerfile
mobilemarketingapigw:
image: eshop/ocelotapigw:${TAG:-latest}
build:
context: .
dockerfile: src/ApiGateways/ApiGw-Base/Dockerfile
webshoppingapigw:
image: eshop/ocelotapigw:${TAG:-latest}
build:
context: .
dockerfile: src/ApiGateways/ApiGw-Base/Dockerfile
webmarketingapigw:
image: eshop/ocelotapigw:${TAG:-latest}
build:
context: .
dockerfile: src/ApiGateways/ApiGw-Base/Dockerfile
yml
mobileshoppingapigw:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- IdentityUrl=http://identity-api
ports:
- "5200:80"
volumes:
- ./src/ApiGateways/Mobile.Bff.Shopping/apigw:/app/configuration
mobilemarketingapigw:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- IdentityUrl=http://identity-api
ports:
- "5201:80"
volumes:
- ./src/ApiGateways/Mobile.Bff.Marketing/apigw:/app/configuration
webshoppingapigw:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- IdentityUrl=http://identity-api
ports:
- "5202:80"
volumes:
- ./src/ApiGateways/Web.Bff.Shopping/apigw:/app/configuration
webmarketingapigw:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- IdentityUrl=http://identity-api
ports:
- "5203:80"
volumes:
- ./src/ApiGateways/Web.Bff.Marketing/apigw:/app/configuration
Figura 6-34. El único archivo necesario para definir cada puerta de enlace de API y BFF
con Ocelot es un archivo de configuración
Al dividir la puerta de enlace de API en varias, cada equipo de desarrollo centrado en
otros subconjuntos de microservicios puede administrar sus propias puertas de enlace
de API mediante archivos de configuración de Ocelot independientes. Además, al
mismo tiempo pueden reutilizar la misma imagen de Docker de Ocelot.
Figura 6-35. Acceso a un microservicio a través de una dirección URL proporcionada por
la puerta de enlace de API
Pero la aplicación está configurada para que acceda a todos los microservicios a través
de las puertas de enlace de API, no a través de "accesos directos" de puerto directo.
El patrón de agregación de puertas de enlace en
eShopOnContainers
Como se mencionó anteriormente, una manera flexible de implementar la agregación
de solicitudes consiste en usar servicios personalizados, mediante código. También se
podría implementar la agregación de solicitudes con la característica de agregación de
solicitudes en Ocelot , pero es posible que no sea tan flexible como se necesita. Por
tanto, el método seleccionado para implementar la agregación en eShopOnContainers
es mediante un servicio de API web de ASP.NET Core explícito para cada agregador.
Al ampliar más el área empresarial "Shopping" de la imagen siguiente, se puede ver que
al usar los servicios agregadores de las puertas de enlace de API se reduce el
intercambio de mensajes entre las aplicaciones cliente y los microservicios.
Figura 6-38. Visión ampliada de los servicios de agregador
El caso del área empresarial "Marketing" y los microservicios es un caso de uso simple,
por lo que no hay necesidad de usar agregadores, aunque se podría, si fuera necesario.
Dado que en eShopOnContainers se usan varias puertas de enlace de API con límites
basados en BFF y áreas de negocio, el servicio Identity/Auth se excluye de las puertas de
enlace de API, como se resalta en color amarillo en el diagrama siguiente.
Pero Ocelot también admite que el microservicio Identity/Auth se sitúe dentro de los
límites de la puerta de enlace de API, como se muestra en este otro diagrama.
Tal y como se muestra en el diagrama anterior, cuando el microservicio Identity está por
debajo de la puerta de enlace de API (AG): 1) La puerta de enlace de API solicita un
token de autenticación del microservicio Identity; 2) el microservicio Identity devuelve
las solicitudes de token a la puerta de enlace de API; 3-4) solicitudes de los
microservicios a la puerta de enlace de API mediante el token de autenticación. Como
en la aplicación eShopOnContainers se ha dividido la puerta de enlace de API en varios
BFF (back-end para front-end) y puertas de enlace de API de áreas de negocio, otra
opción habría sido crear una puerta de enlace de API adicional para los intereses
transversales. Esa opción sería razonable en una arquitectura basada en microservicios
más compleja con varios microservicios de intereses transversales. Como en
eShopOnContainers solo hay un interés transversal, se ha decidido controlar solamente
el servicio de seguridad fuera del territorio de la puerta de enlace de API, por motivos
de simplicidad.
JSON
{
"DownstreamPathTemplate": "/api/{version}/{everything}",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "basket-api",
"Port": 80
}
],
"UpstreamPathTemplate": "/api/{version}/b/{everything}",
"UpstreamHttpMethod": [],
"AuthenticationOptions": {
"AuthenticationProviderKey": "IdentityApiKey",
"AllowedScopes": []
}
}
namespace OcelotApiGw
{
public class Startup
{
private readonly IConfiguration _cfg;
C#
namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers
{
[Route("api/v1/[controller]")]
[Authorize]
public class BasketController : Controller
{
//...
}
}
ValidAudiences como "basket" se ponen en correlación con el público definido en cada
microservicio con AddJwtBearer() en el método ConfigureServices() de la clase Startup,
como se muestra en el código siguiente.
C#
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = identityUrl;
options.RequireHttpsMetadata = false;
options.Audience = "basket";
});
Si intenta acceder a cualquier microservicio protegido, como Basket, con una dirección
URL de ReRoute basada en la puerta de enlace de API como
http://host.docker.internal:5202/api/v1/b/basket/1 , obtiene un error "401 No
autorizado", a menos que proporcione un token válido. Por otro lado, si una dirección
URL de ReRoute está autenticada, Ocelot invoca a cualquier esquema descendente con
el que esté asociada (la dirección URL de microservicio interna).
JSON
"RouteClaimsRequirement": {
"UserType": "employee"
}
Pero si usa un enfoque de entrada, tendrá una capa intermedia entre Internet y los
servicios (incluidas las puertas de enlace de API), que actúa como un proxy inverso.
Como definición, una entrada es una colección de reglas que permiten que las
conexiones entrantes lleguen a los servicios de clúster. Una entrada se configura para
proporcionar a los servicios direcciones URL accesibles de forma externa, tráfico con
equilibrio de carga, terminación SSL y mucho más. Los usuarios solicitan la entrada
mediante la publicación del recurso de entrada en el servidor de API.
Las puertas de enlace de API actúan de front-end o fachadas en las que solo se exponen
los servicios, pero no las aplicaciones web que suelen estar fuera de su ámbito. Además,
es posible que las puertas de enlace de API oculten ciertos microservicios internos.
Pero la entrada simplemente redirige las solicitudes HTTP pero no intenta ocultar
ningún microservicio ni aplicación web.
Tener un nivel Nginx de entrada en Kubernetes delante de las aplicaciones web además
de las distintas puertas de enlace de API de Ocelot o BFF es la arquitectura ideal, como
se muestra en el diagrama siguiente.
Figura 6-41. El nivel de entrada en eShopOnContainers cuando se implementa en
Kubernetes
Una entrada de Kubernetes actúa como un proxy inverso para todo el tráfico a la
aplicación, incluidas las aplicaciones web, que están fuera del ámbito de la puerta de
enlace de la API. Al implementar eShopOnContainers en Kubernetes, solo expone
algunos servicios o puntos de conexión a través de la entrada, básicamente la lista
siguiente de postfijos en las direcciones URL:
comprobaciones de estado
/webshoppingapigw para el BFF web y los procesos empresariales de compra
empresariales de compra
/mobilemarketingapigw para el BFF para dispositivos móviles y los procesos
empresariales de marketing
Limitación de frecuencia
https://ocelot.readthedocs.io/en/latest/features/ratelimiting.html
Anterior Siguiente
Abordar la complejidad empresarial en
un microservicio con patrones DDD y
CQRS
Artículo • 15/02/2023
Sugerencia
Descargar PDF
Diseñe un modelo de dominio para cada microservicio o contexto limitado que refleje un
conocimiento del ámbito empresarial.
Pero la mayoría de las técnicas para microservicios orientados a datos, (por ejemplo,
cómo implementar un servicio ASP.NET Core Web API o cómo exponer metadatos de
Swagger con Swashbuckle o NSwag) también son aplicables a los microservicios más
avanzados que se implementan internamente con patrones DDD. Esta sección es una
ampliación de las secciones anteriores, ya que la mayoría de las prácticas explicadas
anteriormente también se aplican aquí o a cualquier tipo de microservicio.
Esta sección proporciona en primer lugar detalles sobre los patrones CQRS simplificados
que se usan en la aplicación de referencia eShopOnContainers. Más adelante, obtendrá
información general sobre las técnicas DDD que le permiten encontrar patrones
comunes que puede volver a usar en sus aplicaciones.
DDD es un tema amplio con numerosos recursos para obtener más información. Puede
empezar con libros como Domain-Driven Design (Diseño guiado por el dominio), de
Eric Evans, y materiales adicionales de Vaughn Vernon, Jimmy Nilsson, Greg Young, Udi
Dahan, Jimmy Bogard y muchos otros expertos en DDD y CQRS. Pero, sobre todo, para
aprender a aplicar técnicas DDD, debe recurrir a conversaciones, pizarras interactivas y
sesiones de modelado de dominio con expertos de su ámbito empresarial específico.
Recursos adicionales
Aprendizaje de DDD
Anterior Siguiente
Aplicación de patrones CQRS y DDD
simplificados en un microservicio
Artículo • 24/03/2023
Sugerencia
Descargar PDF
CQRS es un patrón de arquitectura que separa los modelos para leer y escribir datos.
Bertrand Meyer definió originalmente el término relacionado Separación de consultas y
comandos (CQS) en su libro Construcción de software orientado a objetos. La idea
básica es que puede dividir las operaciones de un sistema en dos categorías claramente
diferenciadas:
CQS es un concepto simple: se trata de métodos dentro del mismo objeto que son
consultas o comandos. Cada método devuelve o transforma el estado, pero no ambas
cosas. Incluso un único objeto de patrón de repositorio puede cumplir con CQS. CQS
puede considerarse un principio fundamental para CQRS.
CQRS significa tener dos objetos para una operación de lectura/escritura cuando en
otros contextos solo hay uno. Hay razones para tener una base de datos para las
operaciones de lectura sin normalizar, de la cual puede obtener información en la
bibliografía sobre CQRS más avanzada. Pero aquí no vamos a usar este enfoque, ya que
el objetivo es tener más flexibilidad en las consultas en lugar de limitar las consultas con
las restricciones de patrones de DDD como los agregados.
El microservicio "Ordering" lógico incluye su base de datos Ordering, que puede estar,
pero no tiene que estar, en el mismo host de Docker. La presencia de la base de datos
en el mismo host de Docker es buena para el desarrollo, pero no para producción.
El nivel de aplicación puede ser la propia API web. Aquí, el aspecto de diseño
importante es que el microservicio, siguiendo el patrón de CQRS, ha dividido las
consultas y los ViewModels (modelos de datos creados especialmente para las
aplicaciones cliente) de los comandos, del modelo del dominio y de las transacciones.
Este enfoque mantiene las consultas independientes de las restricciones procedentes de
los patrones DDD que solo tienen sentido para las transacciones y las actualizaciones,
como se explica en secciones posteriores.
Recursos adicionales
Greg Young. Control de versiones en un sistema de origen de eventos (Gratis
para leer libros electrónicos en línea)
https://leanpub.com/esversioning/read
Anterior Siguiente
Aplicación de enfoques CQRS y CQS en
un microservicio DDD en
eShopOnContainers
Artículo • 09/05/2023
Sugerencia
Descargar PDF
Por lo tanto, podría usar un modelo de datos de "lectura" distinto al modelo de dominio
de "escritura" de lógica transaccional, aunque los microservicios de ordenación usen la
misma base de datos. Por lo tanto, se trata de un enfoque CQRS simplificado.
Por otro lado, los comandos, que producen transacciones y actualizaciones de datos,
cambian el estado del sistema. Debe tener cuidado con los comandos cuando trabaje
con la complejidad y las reglas de negocio cambiantes. Ahí es donde se prefieren aplicar
técnicas de DDD para tener un sistema mejor modelado.
Los patrones DDD presentados en esta guía no se deben aplicar de forma general, ya
que establecen restricciones en el diseño. Esas restricciones proporcionan ventajas
como una mayor calidad con el tiempo, especialmente en comandos y otro código que
modifican el estado del sistema. Pero las restricciones agregan complejidad con menos
ventajas para leer y consultar datos.
Como se muestra en la figura 7-2 de la sección anterior, esta guía sugiere usar patrones
DDD solo en el área transaccional o de actualizaciones del microservicio (es decir, como
se desencadena con comandos). Las consultas pueden seguir un enfoque más simple y
deben separarse de los comandos, según un enfoque CQRS.
Para implementar el "lado de consultas", puede elegir entre varios enfoques, desde un
ORM completo como EF Core, proyecciones de AutoMapper, procedimientos
almacenados, vistas, vistas materializadas o un micro ORM.
Al usar este enfoque, las actualizaciones del modelo que afectan al modo en que se
conservan las entidades en una base de datos SQL también necesitan actualizaciones
independientes de las consultas SQL usadas por Dapper o cualquier otro enfoque
independiente (distinto de EF) para las consultas.
Recursos adicionales
Martin Fowler. CQRS
https://martinfowler.com/bliki/CQRS.html
Anterior Siguiente
Implementación de lecturas/consultas
en un microservicio CQRS
Artículo • 09/03/2023
Sugerencia
Descargar PDF
El enfoque más sencillo para el lado de las consultas en un enfoque CQRS simplificado
se puede implementar consultando la base de datos con un Micro-ORM como Dapper,
devolviendo ViewModel dinámicos. Las definiciones de consulta realizan una consulta a
la base de datos y devuelven un ViewModel dinámico creado sobre la marcha para cada
consulta. Puesto que las consultas son idempotentes, no cambian los datos por muchas
veces que ejecute una consulta. Por lo tanto, no es necesario estar restringido por un
patrón DDD usado en el lado transaccional, como agregados y otros patrones, y por eso
las consultas se separan del área transaccional. Consulta la base de datos para obtener
los datos que necesita la interfaz de usuario y devolver un ViewModel dinámico que no
tiene que estar definido estáticamente en ningún lugar (no hay clases para los
ViewModel), excepto en las propias instrucciones SQL.
Puesto que se trata de un método sencillo, el código necesario para el lado de las
consultas (como código que usa un micro ORM como Dapper ) pueden implementarse
dentro del mismo proyecto de API Web . En la figura 7-4 se muestra este enfoque. Las
consultas se definen en el proyecto de microservicio Ordering.API dentro de la solución
eShopOnContainers.
Figura 7-4. Consultas (Queries) en el microservicio de pedidos (Ordering) en
eShopOnContainers
Los datos devueltos (ViewModel) pueden ser el resultado de combinar datos de varias
entidades o tablas de la base de datos, o incluso de varios agregados definidos en el
modelo de dominio para el área transaccional. En este caso, dado que va a crear
consultas independientes del modelo de dominio, se ignoran las restricciones y los
límites de agregados, y se pueden consultar cualquier tabla y columna que necesite.
Este enfoque proporciona gran flexibilidad y productividad a los desarrolladores que
crean o actualizan las consultas.
Los elementos ViewModels pueden ser tipos estáticos definidos en clases (como se
implementa en el microservicio de pedidos). O bien, se pueden crear dinámicamente en
función de las consultas realizadas, lo que resulta ágil para los desarrolladores.
C#
using Dapper;
using Microsoft.Extensions.Configuration;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.Dynamic;
using System.Collections.Generic;
public class OrderQueries : IOrderQueries
{
public async Task<IEnumerable<dynamic>> GetOrdersAsync()
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
return await connection.QueryAsync<dynamic>(
@"SELECT o.[Id] as ordernumber,
o.[OrderDate] as [date],os.[Name] as [status],
SUM(oi.units*oi.unitprice) as total
FROM [ordering].[Orders] o
LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid
LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId =
os.Id
GROUP BY o.[Id], o.[OrderDate], os.[Name]");
}
}
}
Si quiere especificar los tipos de respuesta de Swagger, debe utilizar clases DTO
explícitas como tipo de valor devuelto. Por lo tanto, las clases DTO predefinidas
permiten ofrecer información más completa de Swagger. Eso mejora la documentación
y la compatibilidad de la API al utilizar una API.
En el ejemplo siguiente, puede ver cómo la consulta devuelve datos mediante una clase
ViewModel DTO explícita: la clase OrderSummary.
C#
using Dapper;
using Microsoft.Extensions.Configuration;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.Dynamic;
using System.Collections.Generic;
Lo que más preocupa a los desarrolladores que utilizan API Web y microservicios es lo
que se devuelve, sobre todo los tipos de respuesta y los códigos de error (si no son los
habituales). Los tipos de respuesta se administran en las anotaciones de datos y en los
comentarios XML.
Sin una documentación correcta en la interfaz de usuario de Swagger, el consumidor
desconoce los tipos que se devuelven o los códigos HTTP que se pueden devolver. Este
problema se corrige agregando
Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute, para que Swashbuckle
pueda generar información completa sobre el modelo de devolución y los valores de
API, como se muestra en el siguiente código:
C#
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers
{
[Route("api/v1/[controller]")]
[Authorize]
public class OrdersController : Controller
{
//Additional code...
[Route("")]
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<OrderSummary>),
(int)HttpStatusCode.OK)]
public async Task<IActionResult> GetOrders()
{
var userid = _identityService.GetUserIdentity();
var orders = await _orderQueries
.GetOrdersFromUserAsync(Guid.Parse(userid));
return Ok(orders);
}
}
}
C#
Figura 7-5. Interfaz de usuario de Swagger que muestra los tipos de respuesta y los
posibles códigos de estado HTTP de una API Web
En la ilustración anterior se pueden ver algunos valores de ejemplo basados en los tipos
ViewModel, además de los posibles códigos de estado HTTP que se pueden devolver.
Recursos adicionales
Dapper
https://github.com/StackExchange/dapper-dot-net
Julie Lerman. Puntos de datos: Dapper, Entity Framework y aplicaciones híbridas
(artículo de MSDN magazine)
https://learn.microsoft.com/archive/msdn-magazine/2016/may/data-points-
dapper-entity-framework-and-hybrid-apps
Anterior Siguiente
Diseño de un microservicio orientado a
DDD
Artículo • 09/05/2023
Sugerencia
Descargar PDF
A veces, estos patrones y reglas técnicas de DDD se perciben como obstáculos con una
curva de aprendizaje pronunciada a la hora de implementar opciones de DDD. Pero lo
importante no son los patrones en sí, sino organizar el código para que esté en línea
con los problemas del negocio y utilizar los mismos términos empresariales (lenguaje
ubicuo). Además, las opciones de DDD solo deben aplicarse en el caso de implementar
microservicios complejos con reglas de negocio importantes. Las responsabilidades más
sencillas, como el servicio CRUD, se pueden administrar con enfoques más sencillos.
La clave está en dónde situar los límites al diseñar y definir un microservicio. Los
patrones de DDD le ayudan a comprender la complejidad del dominio. En el modelo de
dominio de cada contexto delimitado, debe identificar y definir las entidades, los
objetos de valor y los agregados que modelan el dominio. Debe crear y perfeccionar un
modelo de dominio que se encuentre dentro de un límite definido por su contexto. Y
esto se hace patente en la forma de un microservicio. Los componentes situados dentro
de esos límites acaban siendo sus microservicios, aunque, en algunos casos, los
contextos delimitados o los microservicios pueden estar compuestos de varios servicios
físicos. El DDD afecta a los límites y, por lo tanto, a los microservicios.
Las tres capas en un microservicio DDD como Ordering. Cada capa es un proyecto de
VS: la capa de aplicación es Ordering.API, el nivel de dominio es Ordering.Domain y el
nivel de infraestructura es Ordering.Infrastructure. Le recomendamos que diseñe el
sistema de modo que cada nivel se comunique solamente con otros niveles
determinados. Este enfoque puede ser más fácil de aplicar si los niveles se implementan
como bibliotecas de clase distintas, porque puede identificar claramente qué
dependencias se establecen entre bibliotecas. Por ejemplo, el nivel de modelo de
dominio no debe depender de ningún otro nivel (las clases del modelo de dominio
deben ser clases de objetos CLR o POCO). Como se muestra en la figura 7-6, la
biblioteca de nivel Ordering.Domain solo tiene dependencias en las bibliotecas de .NET
o en los paquetes NuGet, pero no en otras bibliotecas personalizadas, como la
biblioteca de datos o de persistencia.
Figura 7-6. Los niveles implementados como bibliotecas permiten controlar mejor las
dependencias entre niveles
Los marcos ORM más modernos, como Entity Framework Core, permiten este enfoque,
de forma que las clases de modelo de dominio no se acoplan a la infraestructura. Pero
no siempre se puede disponer de entidades POCO al usar marcos y bases de datos
NoSQL determinados, como actores y colecciones de confianza en Azure Service Fabric.
Además, esto no significa que pueda tomar un modelo diseñado para una base de
datos relacional y moverla directamente a un NoSQL o a una base de datos orientada a
un documento. En algunos modelos de entidad, es posible que el modelo encaje, pero
normalmente no lo hace. Sigue habiendo restricciones que el modelo de entidad debe
cumplir, basándose en la tecnología de almacenamiento y en la tecnología ORM.
El nivel de aplicación
Si pasamos al nivel de aplicación, podemos citar de nuevo el libro de Eric Evans Domain
Driven Design (Diseño guiado por el dominio):
Nivel de aplicación: define los trabajos que se supone que el software debe hacer y
dirige los objetos de dominio expresivo para que resuelvan problemas. Las tareas que
son responsabilidad de este nivel son significativas para la empresa o necesarias para la
interacción con los niveles de aplicación de otros sistemas. Este nivel debe mantenerse
estrecho. No contiene reglas de negocios ni conocimientos, sino que solo coordina
tareas y delega trabajo a colaboraciones de objetos de dominio en el siguiente nivel. No
tiene ningún estado que refleje la situación empresarial, pero puede tener un estado
que refleje el progreso de una tarea para el usuario o el programa.
El nivel de infraestructura
El nivel de infraestructura es la forma en que los datos que inicialmente se conservan en
las entidades de dominio (en la memoria) se guardan en bases de datos o en otro
almacén permanente. Un ejemplo sería usar código de Entity Framework Core para
implementar las clases del patrón de repositorio que usan DBContext para conservar los
datos en una base de datos relacional.
Así, los proyectos y bibliotecas de clases o niveles dependerán, en última instancia, del
nivel de modelo de dominio (biblioteca) y no al revés, como se muestra en la Figura 7-7.
Figura 7-7. Dependencias existentes entre niveles en DDD
Recursos adicionales
Anterior Siguiente
Diseño de un modelo de dominio de
microservicio
Artículo • 24/03/2023
Sugerencia
Descargar PDF
La misma identidad (es decir, el mismo valor de Id , aunque quizás no sea la misma
entidad de dominio) se puede modelar en varios contextos delimitados o microservicios.
Pero eso no implica que la misma entidad, con los mismos atributos y lógica, se
implemente en varios contextos delimitados. En su lugar, las entidades de cada contexto
delimitado limitan sus atributos y comportamientos a los requeridos en el dominio de
ese contexto delimitado.
Por ejemplo, es posible que la entidad de comprador tenga la mayoría de los atributos
de una persona que estén definidos en la entidad de usuario en el microservicio de
perfiles o identidades, incluida la identidad. Pero la entidad de comprador en el
microservicio de pedidos podría tener menos atributos, porque solo determinados
datos del comprador están relacionados con el proceso de pedido. El contexto de cada
microservicio o contexto delimitado afecta a su modelo de dominio.
En la figura 7-8 se muestra una entidad de dominio que implementa no solo los
atributos de datos, sino también las operaciones o los métodos con lógica de dominio
relacionada.
Figura 7-8. Ejemplo de un diseño de entidad de dominio en el que se implementan
datos y comportamiento
El síntoma básico de un modelo de dominio anémico es que a primera vista parece real.
Hay objetos, muchos denominados en función de los nombres del espacio de dominio,
que están conectados con las relaciones enriquecidas y la estructura de los modelos de
dominio reales. Lo interesante aparece cuando se examina el comportamiento y se
descubre que apenas hay comportamiento en estos objetos, lo que los convierte en
poco más que conjuntos de captadores y establecedores.
Por supuesto, cuando se usa un modelo de dominio anémico, esos modelos de datos se
usan desde un conjunto de objetos de servicio (denominado tradicionalmente capa de
negocio) que captura toda la lógica de negocios o de dominio. La capa de negocio se
encuentra en la parte superior del modelo de datos y usa el modelo de datos al igual
que los datos.
Por ese motivo las arquitecturas de microservicios son perfectas para un enfoque de
múltiples arquitecturas según cada contexto delimitado. Por ejemplo, en
eShopOnContainers, el microservicio de pedidos implementa patrones DDD, pero el
microservicio de catálogo, que es un servicio CRUD simple, no lo hace.
Recursos adicionales
DevIQ. Entidad de dominio
https://deviq.com/entity/
Martin Fowler. The Anemic Domain Model (El modelo de dominio anémico)
https://martinfowler.com/bliki/AnemicDomainModel.html
Una entidad requiere una identidad, pero en un sistema hay muchos objetos que no lo
hacen, como el patrón de objeto de valor. Un objeto de valor es un objeto sin identidad
conceptual que describe un aspecto de dominio. Se trata de objetos de los que se crea
una instancia para representar elementos de diseño que solo interesan temporalmente.
Interesa lo que son, no quiénes son. Los números y las cadenas son algunos ejemplos,
pero también pueden ser conceptos de nivel superior como grupos de atributos.
Es posible que algo que sea una entidad en un microservicio no lo sea en otro, porque
en el segundo caso, es posible que el contexto delimitado tenga un significado
diferente. Por ejemplo, una dirección en una aplicación de comercio electrónico podría
no tener ninguna identidad, ya que es posible que solo represente un grupo de
atributos del perfil de cliente para una persona o empresa. En este caso, la dirección se
debería clasificar como un objeto de valor. Pero en una aplicación para una empresa de
energía eléctrica, la dirección del cliente podría ser importante para el dominio de
negocio. Por tanto, la dirección debe tener una identidad para poder vincular el sistema
de facturación directamente con la dirección. En ese caso, una dirección debería
clasificarse como una entidad de dominio.
Una persona con un nombre y unos apellidos normalmente es una entidad debido a
que una persona tiene identidad, aunque el nombre y los apellidos coincidan con otro
conjunto de valores, como sucede si también hacen referencia a otra persona.
Los objetos de valor son difíciles de administrar en bases de datos relacionales y ORM
como Entity Framework (EF), mientras que en las bases de datos orientadas a
documentos son más fáciles de implementar y usar.
Objeto de valor
https://deviq.com/value-object/
El patrón de agregado
Un modelo de dominio contiene grupos de entidades de datos diferentes y procesos
que pueden controlar un área importante de funcionalidad, como el cumplimiento de
pedidos o el inventario. Una unidad de DDD más específica es el agregado, que
describe un clúster o grupo de entidades y comportamientos que se pueden tratar
como una unidad coherente.
El propósito de una raíz agregada es asegurar la coherencia del agregado; debe ser el
único punto de entrada para las actualizaciones del agregado a través de métodos u
operaciones en la clase de raíz agregada. Los cambios en las entidades dentro del
agregado solo se deben realizar a través de la raíz agregada. Se encarga de proteger la
coherencia del agregado, teniendo en cuenta todas los elementos invariables y las
reglas de coherencia con los que tenga que cumplir en el agregado. Si cambia una
entidad secundaria o un objeto de valor por separado, la raíz agregada no podrá
garantizar que el agregado esté en un estado válido. Sería como una mesa con una pata
coja. El propósito principal de la raíz agregada es mantener la coherencia.
C#
Recursos adicionales
Vaughn Vernon. Effective Aggregate Design - Part I: Modeling a Single
Aggregate (Diseño eficaz de agregados - Parte I: modelado de un único
agregado) (de https://dddcommunity.org/ )
https://dddcommunity.org/wp-
content/uploads/files/pdf_articles/Vernon_2011_1.pdf
Vaughn Vernon. Effective Aggregate Design - Part II: Making Aggregates Work
Together (Diseño de agregados efectivo, parte II: Conseguir que los agregados
funcionen juntos) (de https://dddcommunity.org/ )
https://dddcommunity.org/wp-
content/uploads/files/pdf_articles/Vernon_2011_2.pdf
Vaughn Vernon. Effective Aggregate Design - Part III: Gaining Insight Through
Discovery (Diseño de agregado efectivo, parte III: Obtener información por
medio de la detección) (de https://dddcommunity.org/ )
https://dddcommunity.org/wp-
content/uploads/files/pdf_articles/Vernon_2011_3.pdf
Anterior Siguiente
Implementación de un modelo de
dominio de microservicio con .NET
Artículo • 29/03/2023
Sugerencia
Descargar PDF
También puede ver una carpeta SeedWork que contiene clases base personalizadas
que se pueden usar como base para las entidades de dominio y los objetos de valor,
para no tener código redundante en la clase de objeto de cada dominio.
Si abre cualquiera de los archivos de una carpeta de agregado, puede ver que está
marcado como clase base personalizada o interfaz, como entidad u objeto de valor, tal
como se ha implementado en la carpeta SeedWork .
C#
_orderItems.Add(orderItem);
}
// ...
// Additional methods with domain rules/logic related to the Order
aggregate
// ...
}
Tener una raíz de agregado significa que la mayoría del código relacionado con la
coherencia y las reglas de negocio de las entidades del agregado deben implementarse
como métodos en la clase raíz de agregado Order (por ejemplo, AddOrderItem al
agregar un objeto OrderItem al agregado). No debe crear ni actualizar objetos
OrderItems de forma independiente ni directa; la clase AggregateRoot debe mantener el
control y la coherencia de cualquier operación de actualización en sus entidades
secundarias.
En el código anterior, observe que muchos atributos son de solo lectura o privados, y
que solo pueden actualizarlos los métodos de clase, por lo que cualquier actualización
tiene en cuenta las invariables de dominio de negocio de cuenta y la lógica especificada
en los métodos de clase.
Por ejemplo, de acuerdo a los patrones DDD, no debería hacer lo siguiente desde
ningún método de controlador de comando ni clase de capa de aplicación (de hecho
debería ser imposible hacerlo):
C#
// WRONG ACCORDING TO DDD PATTERNS – CODE AT THE APPLICATION LAYER OR
// COMMAND HANDLERS
// Code in command handler methods or Web API controllers
//... (WRONG) Some code with business logic out of the domain classes ...
OrderItem myNewOrderItem = new OrderItem(orderId, productId, productName,
pictureUrl, unitPrice, discount, units);
En este caso, el método Add es puramente una operación para agregar datos, con
acceso directo a la colección OrderItems. Por lo tanto, la mayoría de la lógica, las reglas
o las validaciones del dominio relacionadas con esa operación con las entidades
secundarias se distribuirá a la capa de aplicación (controladores de comandos y
controladores de Web API).
Para seguir los patrones DDD, las entidades no deben tener establecedores públicos en
ninguna propiedad de entidad. Los cambios en una entidad deben realizarse mediante
métodos explícitos con lenguaje ubicuo explícito sobre el cambio que están realizando
en la entidad.
Además, las colecciones de la entidad (por ejemplo, OrderItems) deben ser propiedades
de solo lectura (el método AsReadOnly explicado más adelante). Debe ser capaz de
actualizarla solo desde los métodos de la clase raíz de agregado o los métodos de
entidad secundaria.
Como puede ver en el código de la raíz de agregado Order, todos los establecedores
deben ser privados o al menos de solo lectura externamente para que cualquier
operación en los datos de la entidad o sus entidades secundarias tenga que realizarse
mediante métodos en la clase de entidad. Esto mantiene la coherencia de una manera
controlada y orientada a objetos en lugar de implementar código de script
transaccional.
C#
//...
Cuando use Entity Framework Core 1.1 o posterior, una entidad DDD se puede expresar
mejor porque permite asignar a campos además de a propiedades. Esto resulta útil al
proteger colecciones de entidades secundarias u objetos de valor. Con esta mejora,
puede usar campos privados simples en lugar de propiedades y puede implementar
cualquier actualización de la colección de campos en los métodos públicos y
proporcionar acceso de solo lectura mediante el método AsReadOnly.
Recursos adicionales
Vaughn Vernon. Modeling Aggregates with DDD and Entity Framework
(Modelado de agregados con DDD y Entity Framework). Tenga en cuenta que
esto no es Entity Framework Core.
https://kalele.io/blog-posts/modeling-aggregates-with-ddd-and-entity-
framework/
Udi Dahan. How to create fully encapsulated Domain Models (Cómo crear
modelos de dominio totalmente encapsulados)
https://udidahan.com/2008/02/29/how-to-create-fully-encapsulated-domain-
models/
Anterior Siguiente
Seedwork (interfaces y clases base
reutilizables para su modelo de
dominio)
Artículo • 10/05/2023
Sugerencia
Descargar PDF
La carpeta de soluciones contiene una carpeta SeedWork. Esta carpeta contiene las
clases base personalizadas que puede usar como base de los objetos de valor y las
entidades de dominio. Use estas clases base para no tener código redundante en la
clase de objeto de cada dominio. La carpeta para estos tipos de clases se denomina
SeedWork y no nombres parecidos como Marco. Se llama SeedWork porque la carpeta
contiene solo un pequeño subconjunto de clases reutilizables que realmente no se
puede considerar un marco. Seedwork es un término introducido por Michael Feathers
y popularizado por Martin Fowler , pero esta carpeta también se puede denominar
Common, SharedKernel o similar.
En la Figura 7-12 se muestran las clases que forman el seedwork del modelo de dominio
en el microservicio de pedidos. Tiene algunas clases base personalizadas, como Entity,
ValueObject y Enumeration, además de algunas interfaces. Estas interfaces (IRepository y
IUnitOfWork) informan al nivel de infraestructura de lo que requiere implementación.
Estas interfaces también se usan mediante la inserción de dependencias del nivel de
aplicación.
Figura 7-12. Un conjunto de muestra de interfaces y clases base "seedwork" del modelo
de dominio
C#
Este código, en el que se utiliza una lista de eventos de dominio por entidad, se
describirá en las secciones siguientes, al hablar de los eventos de dominio.
Los repositorios en sí, con el código básico de EF Core o cualquier otra dependencia de
infraestructura y código (Linq, SQL, etc.), no se deben implementar en el modelo de
dominio; los repositorios solo deben implementar las interfaces que defina en el modelo
de dominio.
Otro patrón relacionado con esta práctica (que coloca interfaces de repositorio en el
nivel de modelo de dominio) es el patrón de interfaz separada. Como explica Martin
Fowler, "utilice una interfaz separada para definir una interfaz en un paquete e
implementarla en otro. De esta forma, un cliente que necesite la dependencia en la
interfaz puede no tener en cuenta para nada la implementación".
Seguir el patrón de interfaz separada permite que el nivel de aplicación (en este caso, el
proyecto API web para el microservicio) tenga una dependencia en los requisitos
definidos en el modelo de dominio, pero no una dependencia directa en el nivel de
infraestructura/persistencia. Además, puede usar la inserción de dependencias para
aislar la implementación, que se implementa en el nivel de infraestructura/persistencia
utilizando repositorios.
C#
// Defined at IOrderRepository.cs
public interface IOrderRepository : IRepository<Order>
{
Order Add(Order order);
Recursos adicionales
Martin Fowler. Separated Interface (Interfaz independiente).
https://www.martinfowler.com/eaaCatalog/separatedInterface.html
Anterior Siguiente
Implementación de objetos de valor
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Un objeto de valor puede hacer referencia a otras entidades. Por ejemplo, en una
aplicación que genera una ruta que describe cómo ir de un punto a otro, esa ruta sería
un objeto de valor. Sería una instantánea de puntos en una ruta específica, pero esta
ruta sugerida no tendría una identidad, aunque internamente podría hacer referencia a
entidades como Ciudad, Carretera, etc.
Como se muestra en la figura 7-13, una entidad suele constar de varios atributos. Por
ejemplo, la entidad Order puede modelarse como una entidad con una identidad y
componerse internamente de un conjunto de atributos como OrderId, OrderDate,
OrderItems, etc. En cambio, la dirección, que es simplemente un valor complejo
compuesto por el país o la región, la calle, la ciudad, etc., y que no tiene ninguna
identidad en este dominio, debe modelarse y tratarse como un objeto de valor.
Son inmutables.
La primera característica ya se ha mencionado. La inmutabilidad es un requisito
importante. Los valores de un objeto de valor deben ser inmutables una vez creado el
objeto. Por lo tanto, cuando se construye el objeto, debe proporcionar los valores
necesarios, pero no debe permitir que cambien durante la vigencia del objeto.
C#
return
this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
C#
Puede usar esta clase al implementar el objeto de valor real, al igual que sucede con el
objeto de valor Address que se muestra en el ejemplo siguiente:
C#
public Address() { }
public Address(string street, string city, string state, string country,
string zipcode)
{
Street = street;
City = city;
State = state;
Country = country;
ZipCode = zipcode;
}
Se podría argumentar que los objetos de valor, al ser inmutables, deben ser de solo
lectura (es decir, tener propiedades get-only), y así es. Pero los objetos de valor
normalmente se serializan y deserializan para recorrer colas de mensajes. Asimismo, si
fueran de solo lectura, el deserializador no podría asignar los valores, por lo que
simplemente se dejan como private set , lo cual ofrece un nivel de solo lectura
suficiente para que resulte práctico.
C#
var Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
var two = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
Console.WriteLine(EqualityComparer<Address>.Default.Equals(one, two)); //
True
Console.WriteLine(object.Equals(one, two)); // True
Console.WriteLine(one.Equals(two)); // True
Console.WriteLine( two); // True
Cuando todos los valores son iguales, las comparaciones se evalúan correctamente
como true . Si no ha optado por sobrecargar los operadores == y != , la última
comparación de two se evaluaría como false . Para obtener más información,
vea Sobrecarga de los operadores de igualdad de ValueObject.
C#
Pero la persistencia de ese objeto de valor en la base de datos se efectuaba como una
entidad normal en otra tabla.
Con EF Core 2.0 y versiones posteriores hay nuevos y mejores métodos para conservar
los objetos de valor.
Por convención, se crea una clave principal paralela para el tipo de propiedad y se
asignará a la misma tabla que el propietario mediante la división de tabla. Esto permite
usar tipos de propiedad de forma similar al modo en que se usan los tipos complejos en
EF6 en el .NET Framework tradicional.
Es importante tener en cuenta que los tipos de propiedad nunca se detectan por
convención en EF Core, por lo que se deben declarar explícitamente.
C#
C#
// Part of the OrderEntityTypeConfiguration.cs class
//
public void Configure(EntityTypeBuilder<Order> orderConfiguration)
{
orderConfiguration.ToTable("orders", OrderingContext.DEFAULT_SCHEMA);
orderConfiguration.HasKey(o => o.Id);
orderConfiguration.Ignore(b => b.DomainEvents);
orderConfiguration.Property(o => o.Id)
.ForSqlServerUseSequenceHiLo("orderseq",
OrderingContext.DEFAULT_SCHEMA);
orderConfiguration.Property<DateTime>("OrderDate").IsRequired();
C#
.Property(p=>p.Street).HasColumnName("ShippingStreet");
.Property(p=>p.City).HasColumnName("ShippingCity");
No se admiten los tipos de propiedad opcionales (es decir, que aceptan valores
NULL) que se asignan con el propietario en la misma tabla (es decir, mediante la
división de tablas). Esto se debe a que la asignación se realiza para cada
propiedad; no hay un centinela independiente para el valor complejo NULL como
un todo.
Recursos adicionales
Martin Fowler. ValueObject pattern (El patrón ValueObject)
https://martinfowler.com/bliki/ValueObject.html
Propiedades reemplazadas
https://learn.microsoft.com/ef/core/modeling/shadow-properties
Anterior Siguiente
Uso de las clases de enumeración en
lugar de los tipos de enumeración
Artículo • 10/05/2023
Sugerencia
Descargar PDF
En su lugar, puede crear clases de enumeración que habilitan todas las características
enriquecidas de un lenguaje orientado a objetos.
Sin embargo, esto no es un tema crítico y, en muchos casos, por simplicidad, puede
seguir usando tipos enum normales si lo prefiere. El uso de las clases de enumeración
está más relacionado con los conceptos de tipo empresarial.
C#
protected Enumeration(int id, string name) => (Id, Name) = (id, name);
Puede usar esta clase como un tipo en cualquier entidad u objeto de valor, como ocurre
con la clase CardType : Enumeration siguiente:
C#
Recursos adicionales
Jimmy Bogard. Enumeration classes (Clases de enumeración)
https://lostechies.com/jimmybogard/2008/08/12/enumeration-classes/
Anterior Siguiente
Diseño de validaciones en el nivel de
modelo de dominio
Artículo • 10/05/2023
Sugerencia
Descargar PDF
En el diseño guiado por el dominio (DDD), las reglas de validación se pueden considerar
invariables. La responsabilidad principal de un agregado es aplicar invariables en todos
los cambios de estado para todas las entidades de ese agregado.
Las entidades de dominio siempre deben ser entidades válidas. Hay un número
determinado de invariables para un objeto que siempre deben ser verdaderas. Por
ejemplo, un objeto de un elemento de pedido siempre debe tener una cantidad que
debe constar de un entero positivo, un nombre de artículo y un precio. Por lo tanto, la
aplicación de invariables es responsabilidad de las entidades de dominio (en especial de
la raíz agregada) y un objeto de entidad no debería poder existir si no es válido. Las
reglas invariables se expresan como contratos y, si se infringen, se generan excepciones
o notificaciones.
C#
C#
Aunque, desde la óptica del DDD, el modelo de dominio se ajusta mejor con el uso de
excepciones en los métodos de comportamiento de la entidad o con la implementación
de los patrones de especificación y notificación para aplicar reglas de validación.
Puede resultar lógico usar anotaciones de datos en el nivel de aplicación en las clases
ViewModel (en lugar de hacerlo en las entidades de dominio) que aceptarán la entrada
a fin de permitirlas para la validación del modelo en la capa de la interfaz de usuario,
pero no se debería hacer en la exclusión de validación dentro del modelo de dominio.
Merece la pena mencionar que también se puede usar solo uno de estos patrones (por
ejemplo, validándolo manualmente con instrucciones de control, pero usando el patrón
de notificación para apilar y devolver una lista de errores de validación).
Anterior Siguiente
Validación del lado cliente (validación
de los niveles de presentación)
Artículo • 29/03/2023
Sugerencia
Descargar PDF
La validación del lado cliente es una gran ventaja para los usuarios. Les permite ahorrar
un tiempo que, de otro modo, pasarían esperando un viaje de ida y vuelta al servidor
que podría devolver errores de validación. En términos comerciales, incluso unas pocas
fracciones de segundos multiplicadas por cientos de veces al día suman una gran
cantidad de tiempo, gastos y frustración. La validación inmediata y sencilla permite a los
usuarios trabajar de forma más eficiente y generar una entrada y salida de datos de
mayor calidad.
Al igual que el modelo de vista y el modelo de dominio son diferentes, la validación del
modelo de vista y la validación del modelo de dominio podrían ser similares, pero servir
para un propósito diferente. Si le preocupa DRY (el principio de No repetirse), tenga en
cuenta que en este caso la reutilización del código también puede indicar acoplamiento
y en las aplicaciones empresariales es más importante no acoplar el lado servidor al lado
cliente que seguir el principio DRY.
Incluso cuando se usa la validación del lado cliente, siempre debe validar sus comandos
o DTO de entrada en el código de servidor, ya que las API de servidor son un posible
vector de ataque. Normalmente, la mejor opción es hacer ambas cosas porque, si tiene
una aplicación cliente, desde la perspectiva de la experiencia del usuario es mejor
anticiparse y no permitir que el usuario escriba información no válida.
Por tanto, normalmente se validan los ViewModels en el código del lado cliente.
También puede validar los DTO o los comandos de salida del cliente antes de enviarlos a
los servicios.
Recursos adicionales
En resumen, estos son los conceptos más importantes en lo que respecta a la validación:
Las entidades y los agregados deben aplicar su propia coherencia y ser "siempre
válidos". Las raíces agregadas son responsables de la coherencia de varias
entidades dentro del mismo agregado.
Si cree que una entidad debe entrar en un estado no válido, considere el uso de un
modelo de objetos diferente: por ejemplo, usando un DTO temporal hasta que
cree la entidad de dominio final.
Si necesita crear varios objetos relacionados, como un agregado, y solo son válidos
una vez que todos ellos se han creado, considere la posibilidad de usar el patrón
Factory.
Anterior Siguiente
Eventos de dominio: diseño e
implementación
Artículo • 29/03/2023
Sugerencia
Descargar PDF
Una ventaja importante de los eventos de dominio es que los efectos secundarios se
pueden expresar explícitamente.
Por ejemplo, si simplemente usa Entity Framework y debe haber una reacción a algún
evento, probablemente codificaría cualquier cosa que necesite cerca de lo que
desencadena el evento. Por tanto, la regla se acopla, implícitamente, en el código, y
tendrá que mirar el código para, con suerte, descubrir que la regla se implementa allí.
Por otro lado, el uso de los eventos de dominio hace el concepto explícito, porque hay
un DomainEvent y al menos un DomainEventHandler implicados.
Los eventos de dominio son similares a los eventos de estilo de mensajería, con una
diferencia importante. Con la mensajería real, las colas de mensajes, los agentes de
mensajes o un bus de servicio con AMQP, un mensaje siempre se envía de forma
asincrónica y se comunica entre procesos y equipos. Esto es útil para integrar varios
contextos delimitados, microservicios o incluso otras aplicaciones. Pero con los eventos
de dominio le interesa generar un evento desde la operación de dominio que se está
ejecutando actualmente, pero que los efectos secundarios se produzcan dentro del
mismo dominio.
Los eventos de dominio y sus efectos secundarios (las acciones iniciadas después que se
administren mediante controladores de eventos) se deben producir casi de inmediato,
por lo general en el proceso y dentro del mismo dominio. Por tanto, los eventos de
dominio pueden ser sincrónicos o asincrónicos. Pero los eventos de integración siempre
deben ser asincrónicos.
Por otro lado, el propósito de los eventos de integración es propagar las transacciones
confirmadas y actualizaciones a subsistemas adicionales, con independencia de que
sean otros microservicios, contextos delimitados o incluso aplicaciones externas. Por
tanto, solo se deben producir si la entidad se conserva correctamente, de otra forma
será como si toda la operación nunca se hubiera producido.
Por tanto, la interfaz de bus de eventos necesita una infraestructura que permita la
comunicación entre procesos y distribuida entre servicios potencialmente remotos. Se
pueden basar en un bus de servicio comercial, colas, una base de datos compartida que
se use como un buzón o cualquier otro sistema de mensajería distribuido e, idealmente,
basado en inserciones.
Como alternativa, puede hacer que la raíz agregada se suscriba a los eventos generados
por los miembros de sus agregados (las entidades secundarias). Por ejemplo, cada
entidad secundaria OrderItem puede generar un evento cuando el precio del artículo
sea mayor que una cantidad específica, o bien cuando la cantidad del elemento de
producto sea demasiado alta. Después, la raíz agregada puede recibir esos eventos y
realizar un cálculo o una agregación global.
Este cambio podría provocar nuevos errores y este enfoque también va en contra del
principio abierto/cerrado de SOLID . No solo eso, la clase original que orquestaba
las operaciones no dejaría de crecer, algo contrario al Principio de responsabilidad única
(SRP) .
Por otro lado, si usa eventos de dominio, puede crear una implementación específica y
desacoplada mediante la separación de las responsabilidades de esta manera:
3. Controle los eventos de dominio (en el proceso actual) que van a ejecutar un
número abierto de efectos secundarios en varios agregados o acciones de la
aplicación. Por ejemplo:
C#
Esto es básicamente una clase que contiene todos los datos relacionados con el evento
OrderStarted.
En cuanto al lenguaje ubicuo del dominio, como un evento es algo que tuvo lugar en el
pasado, el nombre de clase del evento se debe representar como un verbo en pasado,
como OrderStartedDomainEvent u OrderShippedDomainEvent. Esta es la forma de
implementar el evento de dominio en el microservicio de pedidos en
eShopOnContainers.
Originalmente, Udi Dahan propuso el uso de una clase estática para administrar y
generar los eventos (por ejemplo, en algunas publicaciones relacionadas, como Domain
Events – Take 2 [Eventos de dominio: Toma 2]). Esto podría incluir una clase estática
denominada DomainEvents que generaría los eventos de dominio inmediatamente
cuando es llamada, con una sintaxis similar a DomainEvents.Raise(Event myEvent) . Jimmy
Bogard escribió una entrada de blog [Strengthening your domain: Domain Events
(Reforzar el dominio: eventos de dominio)] que recomienda un enfoque similar.
Decidir si enviar los eventos de dominio justo antes o justo después de confirmar la
transacción es importante, ya que determina si se van a incluir los efectos secundarios
como parte de la misma transacción o en transacciones diferentes. En este último caso,
debe controlar la coherencia final entre varios agregados. Este tema se analiza en la
sección siguiente.
C#
C#
cardSecurityNumber,
cardHolderName,
cardExpiration);
this.AddDomainEvent(orderStartedDomainEvent);
Tenga en cuenta que lo único que hace el método AddDomainEvent es agregar un
evento a la lista. Todavía no se distribuye ningún evento, ni tampoco se invoca ningún
controlador de eventos.
C#
// EF Core DbContext
public class OrderingContext : DbContext, IUnitOfWork
{
// ...
public async Task<bool> SaveEntitiesAsync(CancellationToken
cancellationToken = default(CancellationToken))
{
// Dispatch Domain Events collection.
// Choices:
// A) Right BEFORE committing data (EF SaveChanges) into the DB.
This makes
// a single transaction including side effects from the domain event
// handlers that are using the same DbContext with Scope lifetime
// B) Right AFTER committing data (EF SaveChanges) into the DB. This
makes
// multiple transactions. You will need to handle eventual
consistency and
// compensatory actions in case of failures.
await _mediator.DispatchDomainEventsAsync(this);
// After this line runs, all the changes (from the Command Handler
and Domain
// event handlers) performed through the DbContext will be committed
var result = await base.SaveChangesAsync();
}
}
Con este código, los eventos de entidad se envían a sus controladores de eventos
correspondientes.
Tenga en cuenta que aquí los límites transaccionales tienen una importancia especial. Si
la unidad de trabajo y la transacción pueden abarcar más de un agregado (como ocurre
cuando se usa EF Core y una base de datos relacional), esto puede funcionar bien. Pero
si la transacción no puede abarcar agregados debe implementar pasos adicionales para
lograr la coherencia. Este es otro motivo por el que la omisión de persistencia no es
universal; depende del sistema de almacenamiento que se use.
No se espera que las reglas que abarcan agregados estén actualizadas en todo
momento. A través del procesamiento de eventos, el procesamiento por lotes u
otros mecanismos de actualización, se pueden resolver otras dependencias dentro
de un periodo determinado. (página 128)
Vaughn Vernon afirma lo siguiente en Effective Aggregate Design. Part II: Making
Aggregates Work Together (Diseño eficaz de agregados - Parte II: hacer que los
agregados funcionen de forma conjunta):
En realidad, ambos enfoques (única transacción atómica y coherencia final) pueden ser
correctos. Realmente depende de los requisitos empresariales o de dominio, y de lo que
los expertos de dominio digan. También depende de la capacidad de escalabilidad que
deba tener el servicio (las transacciones más granulares tienen un impacto menor en
relación con los bloqueos de base de datos). Y depende de la inversión que esté
dispuesto a realizar en el código, puesto que la coherencia final requiere un código más
complejo con el fin de detectar posibles incoherencias entre los agregados y la
necesidad de implementar acciones de compensación. Tenga en cuenta que si confirma
los cambios en el agregado original y después, cuando los eventos se estén
distribuyendo, si se produce un problema y los controladores de eventos no pueden
confirmar sus efectos secundarios, tendrá incoherencias entre los agregados.
C#
C#
public ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler(
ILogger<ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler>
logger,
IBuyerRepository buyerRepository,
IOrderingIntegrationEventService orderingIntegrationEventService)
{
_buyerRepository = buyerRepository ?? throw new
ArgumentNullException(nameof(buyerRepository));
_orderingIntegrationEventService = orderingIntegrationEventService
?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
if (!buyerExisted)
{
buyer = new Buyer(domainEvent.UserId, domainEvent.UserName);
}
buyer.VerifyOrAddPaymentMethod(
cardTypeId,
$"Payment Method on {DateTime.UtcNow}",
domainEvent.CardNumber,
domainEvent.CardSecurityNumber,
domainEvent.CardHolderName,
domainEvent.CardExpiration,
domainEvent.Order.Id);
var buyerUpdated = buyerExisted ?
_buyerRepository.Update(buyer) :
_buyerRepository.Add(buyer);
await _buyerRepository.UnitOfWork
.SaveEntitiesAsync(cancellationToken);
OrderingApiTrace.LogOrderBuyerAndPaymentValidatedOrUpdated(
_logger, buyerUpdated.Id, domainEvent.Order.Id);
}
}
Recursos adicionales
Greg Young. ¿Qué es un evento de dominio?
https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf#page=25
Jimmy Bogard. A better domain events pattern (Un mejor patrón de eventos de
dominio)
https://lostechies.com/jimmybogard/2014/05/13/a-better-domain-events-
pattern/
Vaughn Vernon. Effective Aggregate Design - Part II: Making Aggregates Work
Together (Diseño de agregados efectivo, parte II: Conseguir que los agregados
funcionen juntos)
https://dddcommunity.org/wp-
content/uploads/files/pdf_articles/Vernon_2011_2.pdf
Udi Dahan. How to create fully encapsulated Domain Models (Cómo crear
modelos de dominio totalmente encapsulados)
https://udidahan.com/2008/02/29/how-to-create-fully-encapsulated-domain-
models/
Sugerencia
Descargar PDF
El modelo de repositorio
El patrón de repositorio es un modelo de diseño orientado al dominio y diseñado para
mantener los problemas de persistencia fuera del modelo de dominio del sistema. Una
o varias abstracciones de persistencia (interfaces) se definen en el modelo de dominio, y
estas abstracciones tienen implementaciones en forma de adaptadores específicos de la
persistencia definidos en otra parte de la aplicación.
Las implementaciones de repositorio son clases que encapsulan la lógica necesaria para
tener acceso a orígenes de datos. Centralizan la funcionalidad de acceso a datos
comunes, lo que proporciona un mejor mantenimiento y el desacoplamiento de la
infraestructura o tecnología que se usa para acceder a bases de datos desde el modelo
de dominio. Si se usa un asignador relacional de objetos (ORM) como Entity Framework,
se simplifica el código que se debe implementar, gracias a LINQ y al establecimiento
inflexible de tipos. Esto permite centrarse en la lógica de persistencia de datos en lugar
del establecimiento del acceso a los datos.
Si el usuario realiza cambios, los datos que se van a actualizar proceden de la aplicación
cliente o la capa de presentación al nivel de la aplicación (por ejemplo, un servicio de
API web). Cuando se recibe un comando en un controlador de comandos, se usan
repositorios para obtener los datos que se quieren actualizar desde la base de datos. Se
actualiza en memoria con los datos que se pasa con los comandos y después se
agregan o actualizan los datos (entidades de dominio) en la base de datos a través de
una transacción.
Es importante destacar de nuevo que solo se debe definir un repositorio para cada raíz
agregada, como se muestra en la figura 7-17. Para lograr el objetivo de la raíz agregada
de mantener la coherencia transaccional entre todos los objetos del agregado, nunca se
debe crear un repositorio para cada tabla de la base de datos.
C#
namespace
Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories
{
public class OrderRepository : IOrderRepository
{
// ...
}
}
C#
Pero una manera mejor de que el código aplique la convención de que cada repositorio
esté relacionado con un único agregado consiste en implementar un tipo de repositorio
genérico. De este modo, es explícito que se está usando un repositorio para tener como
destino un agregado concreto. Eso se puede hacer fácilmente mediante la
implementación de una interfaz base IRepository genérica, como se muestra en el
código siguiente:
C#
Como se indicó en una sección anterior, se recomienda definir y colocar las interfaces de
repositorio en el nivel de modelo de dominio para que el nivel de aplicación (como el
microservicio de API web) no dependa directamente del nivel de infraestructura en el
que se han implementado las clases de repositorio reales. Al hacer esto y usar la
inserción de dependencias en los controladores de la API web, puede implementar
repositorios ficticios que devuelven datos falsos en lugar de datos de la base de datos.
Ese enfoque desacoplado permite crear y ejecutar pruebas unitarias que centran la
lógica de la aplicación sin necesidad de conectividad a la base de datos.
Se pueden producir errores en las conexiones a las bases de datos y, más importante
aún, la ejecución de centenares de pruebas en una base de datos no es recomendable
por dos motivos. En primer lugar, puede tardar mucho tiempo debido al gran número
de pruebas. En segundo lugar, puede que los registros de base de datos cambien y
afecten a los resultados de las pruebas, sobre todo si las pruebas se ejecutan en
paralelo, por lo que podrían no ser coherentes. Normalmente, las pruebas unitarias se
pueden ejecutar en paralelo, pero es posible que las pruebas de integración no admitan
la ejecución en paralelo, según su implementación. Realizar pruebas en la base de datos
no es una prueba unitaria sino una prueba de integración. Debería tener muchas
pruebas unitarias que se ejecuten con rapidez, pero menos pruebas de integración
sobre las bases de datos.
Estos operaciones de persistencia múltiples se realizan más adelante en una sola acción
cuando el código del nivel de aplicación lo ordena. La decisión sobre cómo aplicar los
cambios en memoria al almacenamiento de base de datos real normalmente se basa en
el patrón de unidades de trabajo. En EF, el patrón de unidades de trabajo se implementa
mediante DbContext y se ejecuta cuando se realiza una llamada a SaveChanges .
Por ejemplo, Jimmy Bogard, al proporcionar información directa para esta guía, afirmó
lo siguiente:
Los repositorios pueden ser útiles, pero no esenciales para el diseño de DDD, de la
misma forma que el patrón de agregado y un modelo de dominio enriquecido lo son.
Por tanto, use el modelo de repositorio o no, como considere oportuno.
Recursos adicionales
Modelo de repositorio
Edward Hieatt y Rob Mee. Patrón de repositorio.
https://martinfowler.com/eaaCatalog/repository.html
El patrón de Repositorio
https://learn.microsoft.com/previous-versions/msp-n-p/ff649690(v=pandp.10)
Eric Evans. "Domain-Driven Design: Tackling Complexity in the Heart of
Software" (Diseño orientado al dominio: abordar la complejidad en el corazón
del software). (Libro; incluye un debate sobre el patrón de repositorio)
https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-
Software/dp/0321125215/
Anterior Siguiente
Implementación del nivel de
persistencia de infraestructura con
Entity Framework Core
Artículo • 29/03/2023
Sugerencia
Descargar PDF
Entity Framework hace mucho tiempo que forma parte de .NET Framework. Al utilizar
.NET, también debe usar Entity Framework Core, que se ejecuta en Windows o Linux de
la misma manera que .NET. EF Core es una reescritura completa de Entity Framework,
que se implementa con una superficie mucho menor y con mejoras importantes en el
rendimiento.
Recursos adicionales
Entity Framework Core
https://learn.microsoft.com/ef/core/
Clase DbContext
https://learn.microsoft.com/dotnet/api/microsoft.entityframeworkcore.dbcontext
Según los patrones DDD, debe encapsular las reglas y el comportamiento de dominio
dentro de la misma clase de entidad, por lo que puede controlar las invariantes, las
validaciones y las reglas al acceder a cualquier colección. Por lo tanto, en DDD no se
recomienda permitir el acceso público a colecciones de entidades secundarias u objetos
de valor. En cambio, es interesante exponer métodos que controlen cómo y cuándo se
pueden actualizar los campos y las colecciones de propiedades, y qué comportamiento
y qué acciones se producirán cuando esto ocurra.
Desde la versión 1.1 de EF Core, para satisfacer estos requisitos de DDD, puede tener
campos sin formato en las entidades en lugar de propiedades públicas. Si no quiere que
se pueda acceder a un campo de entidad desde el exterior, solo puede crear un campo
o un atributo en vez de una propiedad. También puede utilizar establecedores de
propiedades privadas.
De forma parecida, ahora puede tener acceso de solo lectura a las colecciones usando
una propiedad pública del tipo IReadOnlyCollection<T> , que está respaldada por un
miembro de campo privado para la colección (como List<T> ) en la entidad que se basa
en EF para la persistencia. En las versiones anteriores de Entity Framework, se requerían
propiedades de colección para admitir ICollection<T> , lo que significaba que cualquier
desarrollador que usara la clase de entidad primaria podía agregar o quitar elementos a
través de sus colecciones de propiedades. Esa posibilidad iría en contra de los patrones
recomendados en DDD.
Puede usar una colección privada al mismo tiempo que expone un objeto
IReadOnlyCollection<T> de solo lectura, como se muestra en el ejemplo de código
siguiente:
C#
protected Order() { }
C#
var navigation =
orderConfiguration.Metadata.FindNavigation(nameof(Order.OrderItems));
// Other configuration
}
}
C#
// using directives...
namespace
Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories
{
public class BuyerRepository : IBuyerRepository
{
private readonly OrderingContext _context;
public IUnitOfWork UnitOfWork
{
get
{
return _context;
}
}
return buyer;
}
}
}
Pero los métodos de consulta reales para obtener los datos que se van a enviar al nivel
de presentación o a las aplicaciones cliente se implementan, como se ha mencionado,
en las consultas CQRS basadas en consultas flexibles mediante Dapper.
En la figura 7-18 puede ver las diferencias entre no usar repositorios (directamente
mediante DbContext de EF) y usar repositorios que faciliten la simulación de los
repositorios.
Más adelante, cuando nos centremos en el nivel de aplicación, verá cómo funciona la
inserción de dependencias en ASP.NET Core y cómo se implementa al utilizar
repositorios.
Para ello, hay que establecer la duración del servicio de la instancia del objeto
DbContext en ServiceLifetime.Scoped. Se trata de la duración predeterminada al
C#
builder.Services.AddEntityFrameworkSqlServer()
.AddDbContext<OrderingContext>(options =>
{
options.UseSqlServer(Configuration["ConnectionString"],
sqlOptions =>
sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().
Assembly.GetName().Name));
},
ServiceLifetime.Scoped // Note that Scoped is the default choice
// in AddDbContext. It is shown here only for
// pedagogic purposes.
);
El modo de creación de instancias de DbContext no se debe configurar como
ServiceLifetime.Transient o ServiceLifetime.Singleton.
C#
Recursos adicionales
Implementación de los patrones de repositorio y unidad de trabajo en una
aplicación ASP.NET MVC
https://www.asp.net/mvc/overview/older-versions/getting-started-with-ef-5-
using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-
net-mvc-application
Por convención, cada entidad se configurará para asignarse a una tabla que tenga el
mismo nombre que la propiedad DbSet<TEntity> que expone la entidad en el contexto
derivado. Si no se proporciona ningún valor DbSet<TEntity> a la entidad determinada,
se utiliza el nombre de clase.
Las anotaciones de datos se utilizan en las mismas clases del modelo de entidad, lo que
supone un método más intrusivo desde el punto de vista de DDD. Esto es así porque el
modelo se contamina con anotaciones de datos relacionadas con la base de datos de la
infraestructura. Por otro lado, la API fluida es una forma práctica de cambiar la mayoría
de convenciones y asignaciones en el nivel de infraestructura de la persistencia de
datos, por lo que el modelo de entidad estará limpio y desacoplado de la infraestructura
de persistencia.
C#
// At OrderingContext.cs from eShopOnContainers
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ...
modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
// Other entities' configuration ...
}
orderConfiguration
.Property<int?>("_buyerId")
.UsePropertyAccessMode(PropertyAccessMode.Field)
.HasColumnName("BuyerId")
.IsRequired(false);
orderConfiguration
.Property<DateTime>("_orderDate")
.UsePropertyAccessMode(PropertyAccessMode.Field)
.HasColumnName("OrderDate")
.IsRequired();
orderConfiguration
.Property<int>("_orderStatusId")
.UsePropertyAccessMode(PropertyAccessMode.Field)
.HasColumnName("OrderStatusId")
.IsRequired();
orderConfiguration
.Property<int?>("_paymentMethodId")
.UsePropertyAccessMode(PropertyAccessMode.Field)
.HasColumnName("PaymentMethodId")
.IsRequired(false);
orderConfiguration.Property<string>
("Description").IsRequired(false);
var navigation =
orderConfiguration.Metadata.FindNavigation(nameof(Order.OrderItems));
orderConfiguration.HasOne<PaymentMethod>()
.WithMany()
.HasForeignKey("_paymentMethodId")
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
orderConfiguration.HasOne<Buyer>()
.WithMany()
.IsRequired(false)
.HasForeignKey("_buyerId");
Puede establecer todas las asignaciones de la API fluida dentro del mismo método
OnModelCreating , pero se aconseja crear particiones en el código y tener varias clases de
configuración, una por cada entidad, tal y como se muestra en el ejemplo. En particular
en el caso de los modelos grandes, es aconsejable tener clases de configuración
independientes para configurar diferentes tipos de entidad.
El algoritmo Hi-Lo es útil cuando se necesitan claves únicas antes de confirmar los
cambios. A modo de resumen, el algoritmo Hi-Lo asigna identificadores únicos a filas de
la tabla, pero no depende del almacenaje inmediato de la fila en la base de datos. Esto
le permite empezar a usar los identificadores de forma inmediata, como sucede con los
identificadores de la base de datos secuencial normal.
Obtiene la secuencia de id. por lotes, para minimizar los recorridos de ida y vuelta
a la base de datos.
Genera un identificador que pueden leer los humanos, a diferencia de las técnicas
que utilizan los identificadores GUID.
EF Core admite HiLo con el método UseHiLo , tal t como se muestra en el ejemplo
anterior.
Puede hacerlo con campos únicos o también con colecciones, como si se tratara de un
campo List<> . Este punto se mencionó anteriormente cuando analizamos el modelado
de las clases de modelo de dominio, pero aquí puede ver cómo se realiza esta
asignación con la configuración PropertyAccessMode.Field resaltada en el código
anterior.
C#
C#
La siguiente especificación carga una entidad de cesta única a partir del identificador de
cesta o del identificador del comprador al que pertenece la cesta y realiza una carga
diligente de la colección Items de la cesta.
C#
Por último, puede ver a continuación cómo un repositorio de EF genérico puede usar
una especificación de este tipo para filtrar y cargar de forma diligente los datos
relacionados con un determinado tipo de entidad T.
C#
Además de encapsular la lógica de filtro, puede especificar la forma de los datos que se
van a devolver, incluidas las propiedades que se van a rellenar.
Recursos adicionales
Asignación de tabla
https://learn.microsoft.com/ef/core/modeling/relational/tables
Campos de respaldo
https://learn.microsoft.com/ef/core/modeling/backing-field
Patrón de especificación
https://deviq.com/specification-pattern/
Anterior Siguiente
Uso de bases de datos NoSQL como una
infraestructura de persistencia
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Pero cuando se usa una base de datos NoSQL, especialmente una orientada a
documentos como Azure Cosmos DB, CouchDB o RavenDB, la forma de diseñar el
modelo con agregados DDD es parcialmente similar a cómo se puede hacer en EF Core,
en lo que respecta a la identificación de las raíces agregadas, las clases de entidad
secundarias y las clases de objeto de valor. Pero, en última instancia, la selección de la
base de datos afectará al diseño.
Por ejemplo, en una base de datos orientada a documentos, es correcto que una raíz
agregada tenga varias propiedades de colección secundaria. En una base de datos
relacional, consultar varias propiedades de colección secundaria no está bien
optimizado, porque se recibe una instrucción UNION ALL SQL de EF. Tener el mismo
modelo de dominio para bases de datos relacionales o bases de datos NoSQL no es
sencillo y no debería intentarse. El modelo debe diseñarse entendiendo el uso que se va
a hacer de los datos en cada base de datos en particular.
Una ventaja de utilizar las bases de datos NoSQL es que las entidades estén menos
normalizadas, por lo que no se establece una asignación de tabla. El modelo de dominio
puede ser más flexible que al utilizar una base de datos relacional.
JSON
{
"id": "2024001",
"orderDate": "2/25/2024",
"buyerId": "1234567",
"address": [
{
"street": "100 One Microsoft Way",
"city": "Redmond",
"state": "WA",
"zip": "98052",
"country": "U.S."
}
],
"orderItems": [
{"id": 20240011, "productId": "123456", "productName": ".NET T-
Shirt",
"unitPrice": 25, "units": 2, "discount": 0},
{"id": 20240012, "productId": "123457", "productName": ".NET Mug",
"unitPrice": 15, "units": 1, "discount": 0}
]
}
C#
orderAggregate.UpdateAddress(address);
//Using methods with domain logic within the entity. No anemic-domain model
orderAggregate.AddOrderItem(orderItem1);
// *** End of Domain Model Code ***
// As your app evolves, let's say your object has a new schema. You can
insert
// OrderV2 objects without any changes to the database tier.
Order2 newOrder = GetOrderV2Sample("IdForSalesOrder2");
await client.CreateDocumentAsync(collectionUri, newOrder);
Puede ver que la forma de trabajar con el modelo de dominio puede ser similar a la
manera en que se utiliza en la capa de modelo de dominio cuando la infraestructura es
EF. Se siguen usando los mismos métodos raíz de agregación para garantizar la
coherencia, las invariantes y las validaciones en el agregado.
Pero hay una limitación en Azure Cosmos DB desde un punto de vista del entorno de
desarrollo Docker. Aunque hay un emulador de Azure Cosmos DB local que se puede
ejecutar en una máquina de desarrollo local, este solo es compatible con Windows. No
se admiten Linux ni macOS.
También existe la posibilidad de ejecutar este emulador en Docker, pero solo en los
contenedores de Windows, no en los de Linux. Eso es un impedimento inicial para el
entorno de desarrollo si la aplicación se implementa como contenedores de Linux,
puesto que actualmente no es posible implementar al mismo tiempo contenedores de
Windows y Linux en Docker para Windows. Todos los contenedores que se implementen
tienen que ser de Linux o de Windows.
Figura 7-20. Uso de la API de MongoDB y el protocolo para acceder Azure Cosmos DB
Una ventaja evidente de utilizar la API de MongoDB es que la solución puede ejecutarse
en dos motores de base de datos, MongoDB o Azure Cosmos DB, por lo que sería fácil
migrar a otros entornos. Pero en ocasiones merece la pena usar una API nativa (es decir,
la API de Cosmos DB nativa) con el fin de aprovechar al máximo las capacidades de un
determinado motor de base de datos.
Para comparar el uso de MongoDB frente a Cosmos DB en la nube, consulte las ventajas
de usar Azure Cosmos DB en esta página.
Pero si planea usar la API MongoDB para acceder a Azure Cosmos DB en Azure para
aplicaciones de producción, debe analizar y comparar las diferencias entre las funciones
y el rendimiento al usar la API MongoDB para acceder a bases de datos de Azure
Cosmos DB y usar la API nativa de Azure Cosmos DB. Si el resultado es similar, se puede
utilizar la API de MongoDB, con la ventaja de admitir dos motores de base de datos
NoSQL al mismo tiempo.
En primer lugar, debe definir un modelo que contendrá los datos procedentes de la
base de datos en el espacio de memoria de la aplicación. Este es un ejemplo del modelo
que se usa para Locations en eShopOnContainers.
C#
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver.GeoJsonObjectModel;
using System.Collections.Generic;
coordinatesList)));
}
}
Puede ver que hay unos cuantos atributos y tipos procedentes de los paquetes NuGet
de MongoDB.
Las bases de datos NoSQL suelen ser muy adecuadas para trabajar con datos
jerárquicos no relacionales. En este ejemplo se usan tipos de MongoDB especiales para
ubicaciones geográficas, como GeoJson2DGeographicCoordinates .
Recuperación de la base de datos y la colección
En eShopOnContainers, hemos creado un contexto de base de datos personalizado
donde implementamos el código para recuperar la base de datos y MongoCollections,
como se muestra en el código siguiente.
C#
C#
yml
# docker-compose.override.yml
version: '3.4'
services:
# Other services
locations-api:
environment:
# Other settings
- ConnectionString=${ESHOP_AZURE_COSMOSDB:-mongodb://nosqldata}
El código siguiente muestra el archivo .env con la variable de entorno global de cadena
de conexión de Azure Cosmos DB, tal y como se implementa en eShopOnContainers:
yml
ESHOP_EXTERNAL_DNS_NAME_OR_IP=host.docker.internal
ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP=<YourDockerHostIP>
#ESHOP_AZURE_COSMOSDB=<YourAzureCosmosDBConnData>
yml
# docker-compose.yml
version: '3.4'
services:
# ...Other services...
nosqldata:
image: mongo
Recursos adicionales
Modelado de datos del documento para bases de datos NoSQL
https://learn.microsoft.com/azure/cosmos-db/modeling-data
Vaughn Vernon. The Ideal Domain-Driven Design Aggregate Store? (¿El almacén
de agregado ideal de diseño controlado por dominio?)
https://kalele.io/blog-posts/the-ideal-domain-driven-design-aggregate-store/
Uso del Emulador de Azure Cosmos DB para desarrollo y pruebas de forma local
https://learn.microsoft.com/azure/cosmos-db/local-emulator
Azure Cosmos DB: Uso de MongoChef (Studio 3T) con una cuenta de la API de
MongoDB
https://learn.microsoft.com/azure/cosmos-db/mongodb-mongochef
Anterior Siguiente
Diseño del nivel de aplicación de
microservicios y la API web
Artículo • 10/05/2023
Sugerencia
Descargar PDF
SOLID hace referencia a la forma de diseñar los niveles internos de una aplicación o de
un microservicio, así como a separar las dependencias entre ellas. No está relacionado
con el dominio, sino con el diseño técnico de la aplicación. El principio final, el de
inversión de dependencias, le permite desacoplar el nivel de infraestructura del resto de
niveles, lo que permite una mejor implementación desacoplada de los niveles de DDD.
Siguiendo los principios SOLID, las clases tenderán naturalmente a ser pequeñas, a estar
factorizadas correctamente y a poder probarse fácilmente. Pero, ¿cómo puede saber si
se van a insertar demasiadas dependencias en sus clases? Si usa la inversión de
dependencias a través del constructor, le resultará fácil saberlo con solo mirar el número
de parámetros de su constructor. Si hay demasiadas dependencias, esto suele ser una
señal (una intuición de código ) de que su clase está intentando hacer demasiado y de
que probablemente esté infringiendo el principio de responsabilidad única.
Necesitaríamos otra guía para tratar SOLID con detalle. Para esta guía solo necesita
tener unos conocimientos mínimos de estos temas.
Recursos adicionales
Anterior Siguiente
Implementación del nivel de aplicación
de microservicios mediante la API web
Artículo • 29/03/2023
Sugerencia
Descargar PDF
C#
_orderRepository.Add(order);
Cuando use el contenedor de IoC integrado que proporciona ASP.NET Core, debe
registrar los tipos que quiera insertar en el archivo Program.cs, tal como se muestra en el
código siguiente:
C#
// Register out-of-the-box framework services.
builder.Services.AddDbContext<CatalogContext>(c =>
c.UseSqlServer(Configuration["ConnectionString"]),
ServiceLifetime.Scoped);
builder.Services.AddMvc();
// Register custom application dependencies.
builder.Services.AddScoped<IMyCustomRepository, MyCustomSQLRepository>();
El modelo más común al registrar los tipos en un contenedor de IoC es registrar un par
de tipos: una interfaz y su clase de implementación relacionada. Después, cuando se
solicita un objeto del contenedor de IoC a través de cualquier constructor, se solicita un
objeto de un tipo de interfaz determinado. En el ejemplo anterior, la última línea indica
que, cuando cualquiera de los constructores tiene una dependencia de
IMyCustomRepository (interfaz o abstracción), el contenedor de IoC insertará una
instancia de la clase de implementación MyCustomSQLServerRepository.
Recursos adicionales
C#
Autofac también tiene una característica para analizar ensamblados y registrar tipos por
convenciones de nombre .
El proceso de registro y los conceptos son muy similares a la manera en que se pueden
registrar tipos con el contenedor integrado de IoC de ASP.NET Core, pero cuando se usa
Autofac la sintaxis es un poco diferente.
El tipo de ámbito de la instancia determina cómo se comparte una instancia entre las
solicitudes del mismo servicio o dependencia. Cuando se realiza una solicitud de una
dependencia, el contenedor de IoC puede devolver lo siguiente:
Una única instancia que se comparte entre todos los objetos que usan el
contenedor de IoC (denominada singleton en el contenedor de IoC de ASP.NET
Core).
Recursos adicionales
Introduction to Dependency Injection in ASP.NET Core (Introducción a la
inserción de dependencias en ASP.NET Core)
https://learn.microsoft.com/aspnet/core/fundamentals/dependency-injection
Comparing ASP.NET Core IoC container service lifetimes with Autofac IoC
container instance scopes (Comparación de las duraciones de servicio del
contenedor IoC de ASP.NET Core con ámbitos de instancia de contenedor Autofac
IoC) - Cesar de la Torre.
https://devblogs.microsoft.com/cesardelatorre/comparing-asp-net-core-ioc-
service-life-times-and-autofac-ioc-instance-scopes/
Figura 7-24. Vista general de los comandos o el "lado transaccional" en un patrón CQRS
La clase de comando
Un comando es una solicitud para que el sistema realice una acción que cambia el
estado del sistema. Los comandos son imperativos y se deben procesar una sola vez.
Una característica importante de un comando es que debe procesarse una sola vez por
un único receptor. Esto se debe a que un comando es una única acción o transacción
que se quiere realizar en la aplicación. Por ejemplo, el mismo comando de creación de
pedidos no se debe procesar más de una vez. Se trata de una diferencia importante
entre los comandos y los eventos. Los eventos se pueden procesar varias veces, dado
que es posible que muchos sistemas o microservicios estén interesados en el evento.
Además, es importante que un comando solo se procese una vez en caso de que no sea
idempotente. Un comando es idempotente si se puede ejecutar varias veces sin cambiar
el resultado, ya sea debido a la naturaleza del comando, o bien al modo en que el
sistema lo controla.
Un comando se implementa con una clase que contiene campos de datos o colecciones
con toda la información necesaria para ejecutar ese comando. Un comando es un tipo
especial de objeto de transferencia de datos (DTO), que se usa específicamente para
solicitar cambios o transacciones. El propio comando se basa en la información exacta
que se necesita para procesar el comando y nada más.
C#
[DataContract]
public class CreateOrderCommand
: IRequest<bool>
{
[DataMember]
private readonly List<OrderItemDTO> _orderItems;
[DataMember]
public string UserId { get; private set; }
[DataMember]
public string UserName { get; private set; }
[DataMember]
public string City { get; private set; }
[DataMember]
public string Street { get; private set; }
[DataMember]
public string State { get; private set; }
[DataMember]
public string Country { get; private set; }
[DataMember]
public string ZipCode { get; private set; }
[DataMember]
public string CardNumber { get; private set; }
[DataMember]
public string CardHolderName { get; private set; }
[DataMember]
public DateTime CardExpiration { get; private set; }
[DataMember]
public string CardSecurityNumber { get; private set; }
[DataMember]
public int CardTypeId { get; private set; }
[DataMember]
public IEnumerable<OrderItemDTO> OrderItems => _orderItems;
public CreateOrderCommand()
{
_orderItems = new List<OrderItemDTO>();
}
Básicamente, la clase de comando contiene todos los datos que se necesitan para llevar
a cabo una transacción empresarial mediante los objetos de modelo de dominio. Por
tanto, los comandos son simplemente las estructuras de datos que contienen datos de
solo lectura y ningún comportamiento. El nombre del comando indica su propósito. En
muchos lenguajes como C#, los comandos se representan como clases, pero no son
verdaderas clases en el sentido real orientado a objetos.
Como una característica adicional, los comandos son inmutables, dado que el uso
esperado es que el modelo de dominio los procese directamente. No deben cambiar
durante su duración prevista. En una clase de C#, se puede lograr la inmutabilidad si no
hay establecedores ni otros métodos que cambien el estado interno.
Tenga en cuenta que si quiere o espera que los comandos pasen por un proceso de
serialización o deserialización, las propiedades deben tener un establecedor privado y el
atributo [DataMember] (o [JsonProperty] ). De lo contrario, el deserializador no podrá
reconstruir el objeto en el destino con los valores necesarios. También puede usar
propiedades que realmente sean de solo lectura si la clase tiene un constructor con
parámetros para todas las propiedades, con la convención de nomenclatura de
camelCase habitual, y anotar el constructor como [JsonConstructor] . Sin embargo, esta
opción requiere más código.
Por ejemplo, la clase de comando para crear un pedido probablemente sea similar en
cuanto a los datos del pedido que se quiere crear, pero es probable que no se necesiten
los mismos atributos. Por ejemplo, CreateOrderCommand no tiene un identificador de
pedido, porque el pedido aún no se ha creado.
Muchas clases de comando pueden ser simples y requerir solo unos cuantos campos
sobre algún estado que deba cambiarse. Ese sería el caso si solo se va a cambiar el
estado de un pedido de "en proceso" a "pagado" o "enviado" con un comando similar al
siguiente:
C#
[DataContract]
public class UpdateOrderStatusCommand
:IRequest<bool>
{
[DataMember]
public string Status { get; private set; }
[DataMember]
public string OrderId { get; private set; }
[DataMember]
public string BuyerIdentityGuid { get; private set; }
}
Crea una instancia de la instancia de raíz agregada que es el destino del comando
actual.
El aspecto importante aquí es que cuando se procesa un comando, toda la lógica del
dominio debe incluirse en el modelo de dominio (los agregados), completamente
encapsulada y lista para las pruebas unitarias. El controlador de comandos solo actúa
como una manera de obtener el modelo de dominio de la base de datos y, como último
paso, para indicar al nivel de infraestructura (los repositorios) que conserve los cambios
cuando el modelo cambie. La ventaja de este enfoque es que se puede refactorizar la
lógica del dominio en un modelo de dominio de comportamiento aislado,
completamente encapsulado y enriquecido sin cambiar el código del nivel de aplicación
o infraestructura, que forman el nivel de establecimiento (controladores de comandos,
la API web, repositorios, etc.).
Cuando los controladores de comandos se complican, con demasiada lógica, se puede
producir un problema en el código. Revíselos y, si encuentra lógica de dominio,
refactorice el código para mover ese comportamiento de dominio a los métodos de los
objetos de dominio (la raíz agregada y la entidad secundaria).
C#
_orderRepository.Add(order);
Estos son los pasos adicionales que debe realizar un controlador de comandos:
Usar los datos del comando para funcionar con los métodos y el comportamiento
de la raíz agregada.
Recursos adicionales
Mark Seemann. At the Boundaries, Applications are Not Object-Oriented (En los
límites, las aplicaciones no están orientadas a objetos)
https://blog.ploeh.dk/2011/05/31/AttheBoundaries,ApplicationsareNotObject-
Oriented/
Commands and events (Comandos y eventos)
https://cqrs.nu/faq/Command%20and%20Events
Las otras dos opciones principales, que son las recomendadas, son estas:
El motivo por el que tiene sentido usar el patrón de mediador es que, en las
aplicaciones empresariales, las solicitudes de procesamiento pueden resultar
complicadas. Le interesa poder agregar un número abierto de cuestiones transversales
como registro, validaciones, auditoría y seguridad. En estos casos, puede basarse en una
canalización de mediador (vea Patrón de mediador ) para proporcionar un medio para
estos comportamientos adicionales o cuestiones transversales.
La canalización del comando también puede controlarse mediante una cola de mensajes
de alta disponibilidad para entregar los comandos en el controlador adecuado. El uso
de colas de mensajes para aceptar los comandos puede complicar más la canalización
del comando, ya que probablemente sea necesario dividir la canalización en dos
procesos conectados a través de la cola de mensajes externos. Pero se debe usar si hay
que ofrecer mayor escalabilidad y rendimiento según la mensajería asincrónica. Téngalo
en cuenta en el caso de la figura 7-26, donde el controlador simplemente envía el
mensaje de comando a la cola y vuelve. Después, los controladores de comandos
procesan los mensajes a su propio ritmo. Esa es una gran ventaja de las colas: la cola de
mensajes puede actuar como un búfer en casos en que se necesita hiperescalabilidad
(por ejemplo, para existencias o cualquier otro escenario con un gran volumen de datos
de entrada).
En cambio, debido a la naturaleza asincrónica de las colas de mensajes, debe saber
cómo comunicar a la aplicación cliente si el proceso del comando se ha realizado
correctamente o no. Como norma, nunca debería usar comandos "Fire and Forget"
(dispare y olvídese). Cada aplicación empresarial necesita saber si un comando se ha
procesado correctamente, o al menos se ha validado y aceptado.
[Burtsev Alexey] Veo mucho código en el que la gente usa el control de comandos
asincrónicos o la mensajería de comandos unidireccionales sin ningún motivo para
hacerlo (no están realizando una operación extensa, no ejecutan código asincrónico
externo, ni siquiera cruzan los límites entre aplicaciones para usar bus de mensajes).
¿Por qué agregan esta complejidad innecesaria? Y en realidad, hasta ahora no he
visto ningún ejemplo de código CQRS con controladores de comandos de bloqueo,
aunque funcionaría correctamente en la mayoría de los casos.
En cualquier caso, debe ser una decisión basada en los requisitos empresariales de la
aplicación o el microservicio.
El uso del patrón de mediador ayuda a reducir el acoplamiento y aislar los problemas
del trabajo solicitado, mientras se conecta automáticamente al controlador que lleva a
cabo ese trabajo, en este caso, a controladores de comandos.
En la revisión de esta guía, Jimmy Bogard explica otra buena razón para usar el patrón
de mediador:
Creo que aquí valdría la pena mencionar las pruebas: proporcionan una ventana
coherente al comportamiento del sistema. Solicitud de entrada, respuesta de salida.
Hemos comprobado que es un aspecto muy valioso a la hora de generar pruebas
que se comporten de forma coherente.
C#
[Route("new")]
[HttpPost]
public async Task<IActionResult>
ExecuteBusinessOperation([FromBody]RunOpCommand
runOperationCommand)
{
var commandResult = await _mediator.SendAsync(runOperationCommand);
C#
var createOrderCommand = new CreateOrderCommand(eventMsg.Basket.Items,
eventMsg.UserId,
eventMsg.City,
eventMsg.Street,
eventMsg.State,
eventMsg.Country,
eventMsg.ZipCode,
eventMsg.CardNumber,
eventMsg.CardHolderName,
eventMsg.CardExpiration,
eventMsg.CardSecurityNumber,
eventMsg.CardTypeId);
eventMsg.RequestId);
result = await _mediator.Send(requestCreateOrder);
Sin embargo, este caso también es ligeramente más avanzado porque también se
implementan comandos idempotentes. El proceso CreateOrderCommand debe ser
idempotente, por lo que si el mismo mensaje procede duplicado a través de la red, por
cualquier motivo, como un reintento, el mismo pedido se procesará una sola vez.
Esto se implementa mediante la encapsulación del comando de negocio (en este caso,
CreateOrderCommand) y su inserción en un IdentifiedCommand genérico, cuyo
seguimiento se realiza a través de un identificador de todos los mensajes que lleguen a
través de la red que tienen que ser idempotentes.
En el código siguiente, puede ver que el IdentifiedCommand no es más que un DTO con
un identificador junto con el objeto de comando de negocio insertado.
C#
C#
// IdentifiedCommandHandler.cs
public class IdentifiedCommandHandler<T, R> :
IRequestHandler<IdentifiedCommand<T, R>, R>
where T : IRequest<R>
{
private readonly IMediator _mediator;
private readonly IRequestManager _requestManager;
private readonly ILogger<IdentifiedCommandHandler<T, R>> _logger;
public IdentifiedCommandHandler(
IMediator mediator,
IRequestManager requestManager,
ILogger<IdentifiedCommandHandler<T, R>> logger)
{
_mediator = mediator;
_requestManager = requestManager;
_logger = logger ?? throw new
System.ArgumentNullException(nameof(logger));
}
/// <summary>
/// Creates the result value to return if a previous request was found
/// </summary>
/// <returns></returns>
protected virtual R CreateResultForDuplicateRequest()
{
return default(R);
}
/// <summary>
/// This method handles the command. It just ensures that no other
request exists with the same ID, and if this is the case
/// just enqueues the original inner command.
/// </summary>
/// <param name="message">IdentifiedCommand which contains both original
command & request ID</param>
/// <returns>Return value of inner command or default value if request
same ID was found</returns>
public async Task<R> Handle(IdentifiedCommand<T, R> message,
CancellationToken cancellationToken)
{
var alreadyExists = await _requestManager.ExistAsync(message.Id);
if (alreadyExists)
{
return CreateResultForDuplicateRequest();
}
else
{
await _requestManager.CreateRequestForCommandAsync<T>
(message.Id);
try
{
var command = message.Command;
var commandName = command.GetGenericTypeName();
var idProperty = string.Empty;
var commandId = string.Empty;
switch (command)
{
case CreateOrderCommand createOrderCommand:
idProperty = nameof(createOrderCommand.UserId);
commandId = createOrderCommand.UserId;
break;
default:
idProperty = "Id?";
commandId = "n/a";
break;
}
_logger.LogInformation(
"----- Sending command: {CommandName} - {IdProperty}:
{CommandId} ({@Command})",
commandName,
idProperty,
commandId,
command);
_logger.LogInformation(
"----- Command result: {@Result} - {CommandName} -
{IdProperty}: {CommandId} ({@Command})",
result,
commandName,
idProperty,
commandId,
command);
return result;
}
catch
{
return default(R);
}
}
}
}
C#
// CreateOrderCommandHandler.cs
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, bool>
{
private readonly IOrderRepository _orderRepository;
private readonly IIdentityService _identityService;
private readonly IMediator _mediator;
private readonly IOrderingIntegrationEventService
_orderingIntegrationEventService;
private readonly ILogger<CreateOrderCommandHandler> _logger;
_orderRepository.Add(order);
En el código siguiente se muestra cómo registrar los tipos y comandos del mediador al
usar módulos de Autofac.
C#
builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly)
.AsImplementedInterfaces();
builder.RegisterAssemblyTypes(typeof(CreateOrderCommand).GetTypeInfo().Assem
bly)
.AsClosedTypesOf(typeof(IRequestHandler<,>));
// Other types registration
//...
}
}
C#
C#
builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly)
.AsImplementedInterfaces();
typeof(CreateOrderCommand).GetTypeInfo().Assembly).
AsClosedTypesOf(typeof(IRequestHandler<,>));
// Other types registration
//...
builder.RegisterGeneric(typeof(LoggingBehavior<,>)).
As(typeof(IPipelineBehavior<,>));
builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).
As(typeof(IPipelineBehavior<,>));
}
}
C#
C#
if (failures.Any())
{
throw new OrderingDomainException(
$"Command Validation Errors for type
{typeof(TRequest).Name}",
new ValidationException("Validation exception",
failures));
}
C#
Podría crear validaciones adicionales. Se trata de una forma muy limpia y elegante de
implementar las validaciones de comandos.
El patrón de mediador
Patrón de mediador
https://en.wikipedia.org/wiki/Mediator_pattern
El patrón Decorator
Patrón Decorator
https://en.wikipedia.org/wiki/Decorator_pattern
Put your controllers on a diet: POSTs and commands (Poner los controladores a
dieta: POST y comandos).
https://lostechies.com/jimmybogard/2013/12/19/put-your-controllers-on-a-diet-
posts-and-commands/
CQRS and REST: the perfect match (CQRS y REST: la combinación perfecta)
https://lostechies.com/jimmybogard/2016/06/01/cqrs-and-rest-the-perfect-
match/
Vertical Slice Test Fixtures for MediatR and ASP.NET Core (Accesorios de prueba
de segmentos verticales para MediatR y ASP.NET Core)
https://lostechies.com/jimmybogard/2016/10/24/vertical-slice-test-fixtures-for-
mediatr-and-asp-net-core/
MediatR Extensions for Microsoft Dependency Injection Released (Extensiones
de MediatR para el lanzamiento de inserciones de dependencias de Microsoft)
https://lostechies.com/jimmybogard/2016/07/19/mediatr-extensions-for-
microsoft-dependency-injection-released/
Validación fluida
Anterior Siguiente
Implementación de aplicaciones
resistentes
Artículo • 29/03/2023
Sugerencia
Descargar PDF
Sus aplicaciones basadas en microservicios y en la nube deben estar preparadas para los
errores parciales que seguramente se acabarán produciendo en algún momento. Debe
diseñar su aplicación de modo que sea resistente a estos errores parciales.
) Importante
2 Advertencia
Todos los ejemplos de código e imágenes de esta sección eran válidos antes de
usar Linkerd y no se han actualizado para reflejar el código real actual. Por lo que
tienen sentido en el contexto de esta sección.
Anterior Siguiente
Controlar errores parciales
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Figura 8-3. Error parcial amplificado por los microservicios con cadenas largas de
llamadas HTTP sincrónicas
Además, es fundamental que diseñe las aplicaciones cliente y los microservicios para
controlar los errores parciales, es decir, que compile microservicios y aplicaciones cliente
resistentes.
Anterior Siguiente
Estrategias para controlar errores
parciales
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Para tratar con errores parciales, use una de las estrategias que se describen aquí.
Usar reintentos con retroceso exponencial. Esta técnica ayuda a evitar fallos cortos e
intermitentes mediante la realización de un número determinado de intentos de
llamada en caso de que el servicio no esté disponible solo durante un breve período de
tiempo. Esto puede ocurrir debido a problemas de red intermitentes o cuando un
contenedor o microservicio se mueve a otro nodo del clúster. Pero si estos intentos no
se diseñan correctamente con interruptores, pueden agravar el efecto dominó e incluso
pueden llegar a producir un ataque por denegación de servicio (DoS) .
Solucionar los tiempos de expiración de red. En general, los clientes deben diseñarse
para que no se bloqueen indefinidamente y para que usen siempre los tiempos de
expiración cuando esperen una respuesta. Utilizar tiempos de expiración garantiza que
los recursos nunca se bloqueen indefinidamente.
Proporcionar reservas. En este enfoque, el proceso del cliente realiza una lógica de
reserva cuando falla una solicitud, como devolver los datos almacenados en caché o un
valor predeterminado. Este enfoque es adecuado para las consultas, pero es más
complejo para las actualizaciones o los comandos.
Limitar el número de solicitudes en cola. Los clientes también deben imponer un límite
máximo en la cantidad de solicitudes pendientes que un microservicio de cliente puede
enviar a un servicio determinado. Si se alcanza el límite, probablemente no tenga
sentido realizar más solicitudes y dichos intentos deben generar error inmediatamente.
En cuanto a la implementación, la directiva Aislamiento compartimentado de Polly se
puede usar para cumplir este requisito. Este enfoque es básicamente una limitación en
paralelo con SemaphoreSlim como implementación. También admite una "cola" fuera de
la mampara. Puede perder proactivamente una carga excesiva incluso antes de la
ejecución (por ejemplo, porque se considera que ha llegado al límite de su capacidad).
Esto hace que su respuesta a determinados escenarios de error sea mucho más rápida
que la que tendría un interruptor, puesto que el interruptor espera a que se produzcan
los errores. El objeto BulkheadPolicy de Polly expone hasta qué punto están llenos el
espacio limitado por la mampara y la cola, y ofrece eventos sobre desbordamiento para
que también se puedan utilizar para administrar un escalado horizontal automatizado.
Recursos adicionales
Resiliency patterns (Patrones de resistencia)
https://learn.microsoft.com/azure/architecture/framework/resiliency/reliability-
patterns
Adding Resilience and Optimizing Performance (Agregar resistencia y optimizar
el rendimiento)
https://learn.microsoft.com/previous-versions/msp-n-p/jj591574(v=pandp.10)
Anterior Siguiente
Implementar reintentos con retroceso
exponencial
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Los reintentos con retroceso exponencial son una técnica que reintenta una operación,
con un tiempo de espera que aumenta exponencialmente, hasta que se alcanza un
número máximo de reintentos (el retroceso exponencial ). Esta técnica se basa en el
hecho de que los recursos en la nube pueden no estar disponibles de forma
intermitente durante más de unos segundos por cualquier motivo. Por ejemplo, un
orquestador puede mover un contenedor a otro nodo de un clúster para el equilibrio de
carga. Durante ese tiempo se podrían producir errores en algunas solicitudes. Otro
ejemplo podría ser una base de datos como SQL Azure, que puede moverse a otro
servidor para el equilibrio de carga, lo que haría que la base de datos no estuviera
disponible durante unos segundos.
Anterior Siguiente
Implementación de conexiones SQL
resistentes de Entity Framework Core
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Para Azure SQL DB, Entity Framework (EF) Core ya proporciona la lógica de reintento y
resistencia de conexión de base de datos interna. Pero debe habilitar la estrategia de
ejecución de Entity Framework para cada conexión de DbContext si quiere tener
conexiones resistentes de EF Core.
C#
Sin embargo, si su código inicia una transición con BeginTransaction , define su propio
grupo de operaciones que deben tratarse como unidad. Todo el contenido de la
transacción debe revertirse si se produce un error.
C#
// Save product's data and publish integration event through the Event
Bus
// if price has changed
if (raiseProductPriceChangedEvent)
{
//Create Integration Event to be published through the Event Bus
var priceChangedEvent = new ProductPriceChangedIntegrationEvent(
catalogItem.Id, productToUpdate.Price, oldPrice);
// Publish through the Event Bus and mark the saved event as
published
await _catalogIntegrationEventService.PublishThroughEventBusAsync(
priceChangedEvent);
}
// Just save the updated product because the Product's Price hasn't
changed.
else
{
await _catalogContext.SaveChangesAsync();
}
}
C#
_catalogContext.Database.CurrentTransaction.GetDbTransaction());
});
}
}
C#
Recursos adicionales
Connection Resiliency and Command Interception with EF in an ASP.NET MVC
Application (Resistencia de la conexión e intercepción de comandos con EF en
una aplicación de ASP.NET MVC)
https://learn.microsoft.com/aspnet/mvc/overview/getting-started/getting-started-
with-ef-using-mvc/connection-resiliency-and-command-interception-with-the-
entity-framework-in-an-asp-net-mvc-application
Cesar de la Torre. Using Resilient Entity Framework Core SQL Connections and
Transactions (Usar conexiones y transacciones SQL resistentes de Entity
Framework Core)
https://devblogs.microsoft.com/cesardelatorre/using-resilient-entity-framework-
core-sql-connections-and-transactions-retries-with-exponential-backoff/
Anterior Siguiente
Uso de IHttpClientFactory para
implementar solicitudes HTTP
resistentes
Artículo • 10/05/2023
Sugerencia
Descargar PDF
Otra incidencia a la que los desarrolladores deben hacer frente es cuando se usa una
instancia compartida de HttpClient en procesos de larga duración. En una situación en
la que se crean instancias del HttpClient como un singleton o un objeto estático, los
cambios de DNS no se pueden controlar, tal y como se describe en esta incidencia del
repositorio de GitHub sobre dotnet/runtime.
Para solucionar los problemas mencionados anteriormente y para que las instancias de
HttpClient se puedan administrar, .NET Core 2.1 ha introducido dos enfoques, uno de
los cuales es IHttpClientFactory. Se trata de una interfaz que se usa para configurar y
crear instancias de HttpClient en una aplicación mediante Inserción de dependencias
(DI). También proporciona extensiones para el middleware basado en Polly a fin de
aprovechar los controladores de delegación en HttpClient.
Sugerencia
7 Nota
En pro de la brevedad, esta guía muestra la manera más estructurada para usar
IHttpClientFactory , que consiste en usar clientes con tipo (el patrón de agente de
servicio). Pero todas las opciones están documentadas e incluidas actualmente en este
artículo que trata sobre el uso de HttpClientFactoryIHttpClientFactory.
7 Nota
En el diagrama siguiente se muestra cómo se usan los clientes con tipo con
IHttpClientFactory :
Figura 8-4. Uso de IHttpClientFactory con clases de cliente con tipo.
C#
// Program.cs
//Add http client services at ConfigureServices(IServiceCollection services)
builder.Services.AddHttpClient<ICatalogService, CatalogService>();
builder.Services.AddHttpClient<IBasketService, BasketService>();
builder.Services.AddHttpClient<IOrderingService, OrderingService>();
C#
En este ejemplo siguiente, puede ver la configuración de una de las directivas anteriores:
C#
Duraciones de HttpClient
Cada vez que se obtiene un objeto HttpClient de IHttpClientFactory , se devuelve una
nueva instancia. Pero cada cliente HttpClient usa un controlador HttpMessageHandler
que IHttpClientFactory agrupa y vuelve a usar para reducir el consumo de recursos,
siempre y cuando la vigencia de HttpMessageHandler no haya expirado.
Los objetos HttpMessageHandler del grupo tienen una duración que es el período de
tiempo que se puede reutilizar una instancia de HttpMessageHandler en el grupo. El valor
predeterminado es de dos minutos, pero se puede invalidar por cada cliente con tipo.
Para ello, llame a SetHandlerLifetime() en el IHttpClientBuilder que se devuelve cuando
se crea el cliente, como se muestra en el siguiente código:
C#
//Set 5 min as the lifetime for the HttpMessageHandler objects in the pool
used for the Catalog Typed Client
builder.Services.AddHttpClient<ICatalogService, CatalogService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
Cada cliente con tipo puede tener configurado su propio valor de duración de
controlador. Establezca la duración en InfiniteTimeSpan para deshabilitar la expiración
del controlador.
C#
Un cliente con tipo es realmente un objeto transitorio, lo que significa que, cada vez que
se necesita uno, se crea una instancia. Recibe una nueva instancia de HttpClient cada
vez que se construye. Pero los objetos HttpMessageHandler del grupo son los objetos
que varias instancias de HttpClient reutilizan.
C#
namespace Microsoft.eShopOnContainers.WebMVC.Controllers
{
public class CatalogController : Controller
{
private ICatalogService _catalogSvc;
BrandFilterApplied,
TypesFilterApplied);
//… Additional code
}
}
}
Hasta ahora, el fragmento de código anterior tan solo muestra el ejemplo de realizar
solicitudes HTTP normales. Pero la "magia" viene en las secciones siguientes, donde se
muestra cómo todas las solicitudes HTTP que realiza HttpClient pueden tener
directivas resistentes como, por ejemplo, reintentos con retroceso exponencial,
disyuntores, características de seguridad que usan tokens de autenticación o incluso
cualquier otra característica personalizada. Y todo esto se puede hacer simplemente
agregando directivas y delegando controladores a los clientes con tipo registrados.
Recursos adicionales
Directrices de HttpClient para .NET
https://learn.microsoft.com/en-
us/dotnet/fundamentals/networking/http/httpclient-guidelines
Sugerencia
Descargar PDF
En los pasos siguientes se muestra cómo usar reintentos HTTP con Polly integrados en
IHttpClientFactory , que se explica en la sección anterior.
Tal como se muestra en las secciones anteriores, debe definir una configuración
HttpClient de cliente con nombre o con tipo en la configuración estándar de la
aplicación Program.cs. Ahora agregamos código incremental que especifica la directiva
para los reintentos HTTP con retroceso exponencial, tal como se indica a continuación:
C#
// Program.cs
builder.Services.AddHttpClient<IBasketService, BasketService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5)) //Set lifetime to five
minutes
.AddPolicyHandler(GetRetryPolicy());
Para tener un enfoque más modular, la directiva de reintentos HTTP se puede definir en
un método independiente en el archivo Program.cs, tal como se muestra en el código
siguiente:
C#
retryAttempt)));
}
Con Polly, se puede definir una directiva de reintentos con el número de reintentos, la
configuración de retroceso exponencial y las acciones necesarias cuando se produce
una excepción de HTTP, como registrar el error. En este caso, la directiva está
configurada para intentar seis veces con un reintento exponencial, a partir de dos
segundos.
C#
Recursos adicionales
Retry pattern (Patrón de
reintento)https://learn.microsoft.com/azure/architecture/patterns/retry
Polly e IHttpClientFactoryhttps://github.com/App-vNext/Polly/wiki/Polly-and-
HttpClientFactory
Anterior Siguiente
Implementación del patrón de
interruptor
Artículo • 29/03/2023
Sugerencia
Descargar PDF
Tal y como se indicó anteriormente, debe controlar los errores que pueden comportar
un tiempo variable de recuperación, como puede suceder al intentar conectarse a un
recurso o servicio remoto. Controlar este tipo de error puede mejorar la estabilidad y la
resistencia de una aplicación.
Pero también puede haber situaciones en que los errores se deban a eventos
imprevistos que pueden tardar mucho más tiempo en corregirse. La gravedad de estos
errores puede ir desde una pérdida parcial de conectividad hasta el fallo total del
servicio. En estas situaciones, no tiene sentido que una aplicación reintente
continuamente una operación que es probable que no se lleve a cabo correctamente.
Lo que debe hacer la aplicación es codificarse para aceptar que la operación ha fallado y
controlar el error en consecuencia.
El uso de los reintentos HTTP de forma descuidada podría crear ataques por denegación
de servicio (DoS ) dentro de su propio software. Cuando se produce un error en un
microservicio o se ejecuta lentamente, es posible que varios clientes reintenten
solicitudes con error de forma repetida. Eso genera un riesgo peligroso de que el tráfico
destinado al servicio con errores aumente de manera exponencial.
Por tanto, se necesita algún tipo de barrera de defensa para que se detengan las
solicitudes excesivas cuando ya no tiene sentido seguir intentándolo. Esa barrera de
defensa es precisamente el interruptor.
En este caso, lo único que se agrega al código que se usa para los reintentos de llamada
HTTP es el código en el que se agrega la directiva de interruptor a la lista de directivas
que se van a usar, tal como se muestra en el código incremental siguiente.
C#
// Program.cs
var retryPolicy = GetRetryPolicy();
var circuitBreakerPolicy = GetCircuitBreakerPolicy();
builder.Services.AddHttpClient<IBasketService, BasketService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5)) // Sample: default
lifetime is 2 minutes
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>()
.AddPolicyHandler(retryPolicy)
.AddPolicyHandler(circuitBreakerPolicy);
C#
// also in Program.cs
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
}
Los interruptores también se deben usar para redirigir las solicitudes a una
infraestructura de reserva siempre que haya tenido problemas en un recurso concreto
implementado en otro entorno que no sea el de la aplicación cliente o del servicio que
realiza la llamada HTTP. De este modo, si se produce una interrupción en el centro de
datos que afecta solo a los microservicios de back-end, pero no a las aplicaciones
cliente, estas aplicaciones pueden redirigir a los servicios de reserva. Polly está creando
una directiva nueva para automatizar este escenario de directiva de conmutación por
error .
Todas estas características sirven para los casos en los que se administra la conmutación
por error desde el código .NET, y no cuando Azure lo hace de forma automática, con la
transparencia de ubicación.
Desde un punto de vista del uso, al utilizar HttpClient no hay necesidad de agregar nada
nuevo aquí porque el código es el mismo que cuando se usa HttpClient con
IHttpClientFactory , como se mostró en las secciones anteriores.
Este tipo de error también puede darse en el inicio, cuando la aplicación se está
implementando en la nube. En ese caso, podría ser que los orquestadores movieran los
contenedores de un nodo o máquina virtual a otro (iniciando así nuevas instancias) al
repartir equitativamente los contenedores entre los nodos de clúster.
GET http://localhost:5103/failing?enable
GET http://localhost:5103/failing?disable
Esta solicitud deshabilita el middleware.
http://localhost:5103/failing?enable
Figura 8-5. Comprobación del estado del middleware ASP.NET "con errores": en este
caso, deshabilitado.
C#
Aquí tiene un resumen. La directiva de reintentos intenta realizar la solicitud HTTP varias
veces y obtiene errores HTTP. Cuando el número de reintentos alcanza el número
máximo establecido para la directiva del interruptor (en este caso, 5), la aplicación
genera una excepción BrokenCircuitException. El resultado es un mensaje descriptivo,
como el que se muestra en la Figura 8-6.
Puede implementar otra lógica que indique cuándo se debe abrir o interrumpir el
circuito. También puede probar una solicitud HTTP en un microservicio de back-end
distinto si se dispone de un centro de datos de reserva o un sistema back-end
redundante.
Por último, otra posibilidad para CircuitBreakerPolicy consiste en usar Isolate (que
fuerza y mantiene la apertura del circuito) y Reset (que lo cierra de nuevo). Estas
características se pueden utilizar para crear un punto de conexión HTTP de utilidad que
invoque Aislar y Restablecer directamente en la directiva. Este tipo de punto de
conexión HTTP, protegido adecuadamente, también se puede usar en el entorno de
producción para aislar temporalmente un sistema de nivel inferior, como cuando quiere
actualizarlo. También puede activar el circuito manualmente para proteger un sistema
de nivel inferior que le parezca que está fallando.
Recursos adicionales
Circuit Breaker pattern (Patrón Circuit Breaker)
https://learn.microsoft.com/azure/architecture/patterns/circuit-breaker
Anterior Siguiente
Supervisión del estado
Artículo • 20/03/2023
Sugerencia
Descargar PDF
En el modelo típico, los servicios envían informes sobre su estado. Esa información se
agrega para proporcionar una visión general del estado de la aplicación. Si se utiliza un
orquestador, se puede proporcionar información de estado al clúster del orquestador a
fin de que el clúster pueda actuar en consecuencia. Si se invierte en informes de estado
de alta calidad personalizados para la aplicación, se pueden detectar y corregir mucho
más fácilmente los problemas de la aplicación que se está ejecutando.
Implementación de comprobaciones de estado
en servicios de ASP.NET Core
Al desarrollar una aplicación web o microservicio de ASP.NET Core, puede usar la
característica de comprobaciones de estado integrada que se lanzó en ASP.NET Core 2.2
(Microsoft.Extensions.Diagnostics.HealthChecks ). Al igual que muchas características
de ASP.NET Core, las comprobaciones de estado incluyen un conjunto de servicios y un
middleware.
Para usar esta característica con eficacia, primero debe configurar servicios en sus
microservicios. En segundo lugar, necesita una aplicación front-end que realice
consultas para los informes de estado. La aplicación front-end podría ser una aplicación
de informes personalizada, o podría ser un orquestador que reaccione en consecuencia
a los estados.
Para empezar, debe definir qué constituye un estado correcto en cada microservicio. En
la aplicación de ejemplo, definiremos que el estado del microservicio es correcto si se
puede acceder a su API a través de HTTP y si su base de datos de SQL Server
relacionada también está disponible.
En .NET 7, con las API integradas, puede configurar los servicios, y agregar una
comprobación de estado para el microservicio y su base de datos de SQL Server
dependiente de esta forma:
C#
// Program.cs from .NET 7 Web API sample
//...
// Registers required services for health checks
builder.Services.AddHealthChecks()
// Add a health check for a SQL Server database
.AddCheck(
"OrderingDB-check",
new
SqlConnectionHealthCheck(builder.Configuration["ConnectionString"]),
HealthStatus.Unhealthy,
new string[] { "orderingdb" });
C#
if (TestQuery != null)
{
var command = connection.CreateCommand();
command.CommandText = TestQuery;
await command.ExecuteNonQueryAsync(cancellationToken);
}
}
catch (DbException ex)
{
return new HealthCheckResult(status:
context.Registration.FailureStatus, exception: ex);
}
}
return HealthCheckResult.Healthy();
}
}
C#
app.MapHealthChecks("/hc");
Cuando se invoca, el punto de conexión <yourmicroservice>/hc ejecuta todas las
comprobaciones de estado que están configuradas en el método AddHealthChecks() de
la clase Startup y muestra el resultado.
C#
// Extension method from Catalog.api microservice
//
public static IServiceCollection AddCustomHealthCheck(this
IServiceCollection services, IConfiguration configuration)
{
var accountName = configuration.GetValue<string>
("AzureStorageAccountName");
var accountKey = configuration.GetValue<string>
("AzureStorageAccountKey");
hcBuilder
.AddSqlServer(
configuration["ConnectionString"],
name: "CatalogDB-check",
tags: new string[] { "catalogdb" });
if (!string.IsNullOrEmpty(accountName) &&
!string.IsNullOrEmpty(accountKey))
{
hcBuilder
.AddAzureBlobStorage(
$"DefaultEndpointsProtocol=https;AccountName=
{accountName};AccountKey={accountKey};EndpointSuffix=core.windows.net",
name: "catalog-storage-check",
tags: new string[] { "catalogstorage" });
}
if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
{
hcBuilder
.AddAzureServiceBusTopic(
configuration["EventBusConnection"],
topicName: "eshop_event_bus",
name: "catalog-servicebus-check",
tags: new string[] { "servicebus" });
}
else
{
hcBuilder
.AddRabbitMQ(
$"amqp://{configuration["EventBusConnection"]}",
name: "catalog-rabbitmqbus-check",
tags: new string[] { "rabbitmqbus" });
}
return services;
}
// HealthCheck middleware
app.UseHealthChecks("/hc", new HealthCheckOptions()
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
En esa prueba, puede ver que el estado del microservicio Catalog.API (que se ejecuta
en el puerto 5101) es correcto. Se devuelve el código de estado HTTP 200 e información
de estado en JSON. El servicio también comprobó el estado de su dependencia de la
base de datos de SQL Server y RabbitMQ, por lo que el estado se notificó como
correcto.
Uso de guardianes
Un guardián es un servicio independiente que puede observar el estado y la carga en
varios servicios, e informar del estado de los microservicios con una consulta con la
biblioteca HealthChecks vista anteriormente. Esto puede ayudar a evitar errores que no
se detectarían si se observase un único servicio. Los guardianes también son un buen
lugar para hospedar código que lleve a cabo acciones correctoras para condiciones
conocidas sin la intervención del usuario.
El ejemplo de eShopOnContainers contiene una página web que muestra informes de
comprobación de estado de ejemplo, como se muestra en la figura 8-9. Se trata del
guardián más sencillo que se puede tener, dado que lo único que hace es mostrar el
estado de las aplicaciones web y los microservicios en eShopOnContainers.
Normalmente, un guardián también realiza acciones cuando detecta estados no
correctos.
En resumen, este servicio de vigilancia consulta cada uno de los puntos de conexión
"/hc" del microservicio. El middleware ejecutará todas las comprobaciones de estado
definidas en él y devolverá un estado general que dependerá de todas esas
comprobaciones. HealthChecksUI es fácil de usar con algunas entradas de configuración
y dos líneas de código que deben agregarse en el archivo Startup.cs del servicio de
inspección.
JSON
// Configuration
{
"HealthChecksUI": {
"HealthChecks": [
{
"Name": "Ordering HTTP Check",
"Uri": "http://host.docker.internal:5102/hc"
},
{
"Name": "Ordering HTTP Background Check",
"Uri": "http://host.docker.internal:5111/hc"
},
//...
]}
}
C#
Otro aspecto del estado del servicio es informar de las métricas del servicio. Se trata de
una característica avanzada del modelo de estado de algunos orquestadores, como
Service Fabric. Las métricas son importantes cuando se usa un orquestador porque se
usan para equilibrar el uso de recursos. Las métricas también pueden ser un indicador
del estado del sistema. Pongamos por ejemplo una aplicación que tenga muchos
microservicios, y cada instancia informa sobre una métrica de solicitudes por segundo
(RPS). Si un servicio está utilizando más recursos (memoria, procesador, etc.) que otro
servicio, el orquestador puede mover las instancias del servicio en el clúster para
intentar equilibrar el uso de los recursos.
Por último, si almacena todos los flujos de eventos, se puede utilizar Microsoft Power BI
u otras soluciones como Kibana o Splunk para visualizar los datos.
Recursos adicionales
HealthChecks and HealthChecks UI for ASP.NET Core (HealthChecks e interfaz de
usuario de HealthChecks para ASP.NET Core)
https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks
Introduction to Service Fabric health monitoring (Introducción al seguimiento de
estado de Service Fabric)
https://learn.microsoft.com/azure/service-fabric/service-fabric-health-introduction
Azure Monitor
https://azure.microsoft.com/services/monitor/
Anterior Siguiente
Protección de microservicios y
aplicaciones web .NET
Artículo • 15/02/2023
Sugerencia
Descargar PDF
Hay tantos aspectos sobre la seguridad de los microservicios y las aplicaciones web que
se podrían escribir varios libros como este al respecto. Por tanto, en esta sección nos
centraremos en la autenticación, la autorización y los secretos de aplicación.
Implementación de la autenticación en
microservicios y aplicaciones web .NET
A menudo es necesario que los recursos y las API publicados por un servicio se limiten a
determinados usuarios o clientes de confianza. El primer paso para tomar este tipo de
decisiones de confianza en el nivel de API es la autenticación. La autenticación es el
proceso de comprobar de forma fiable la identidad de un usuario.
Sugerencia
C#
services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddRazorPages();
//...
}
C#
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//...
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
//...
}
) Importante
Las líneas del código anterior DEBEN ESTAR EN EL ORDEN MOSTRADO para que
Identity funcione correctamente.
C#
services.AddAuthentication()
.AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.ClientId =
Configuration["Authentication:Microsoft:ClientId"];
microsoftOptions.ClientSecret =
Configuration["Authentication:Microsoft:ClientSecret"];
})
.AddGoogle(googleOptions => { ... })
.AddTwitter(twitterOptions => { ... })
.AddFacebook(facebookOptions => { ... });
//...
}
Proveedor Paquete
Microsoft Microsoft.AspNetCore.Authentication.MicrosoftAccount
Google Microsoft.AspNetCore.Authentication.Google
Facebook Microsoft.AspNetCore.Authentication.Facebook
Twitter Microsoft.AspNetCore.Authentication.Twitter
Sugerencia
También puede crear middleware de autenticación externo propio para resolver alguna
necesidad especial.
Por ejemplo, en una Web API de ASP.NET Core que expone puntos de conexión RESTful
a los que podrían tener acceso aplicaciones de una sola página (SPA), clientes nativos o
incluso otras Web API, normalmente le interesa usar la autenticación mediante token de
portador. Estos tipos de aplicaciones no funcionan con cookies, pero pueden recuperar
fácilmente un token de portador e incluirlo en el encabezado de autorización de las
solicitudes posteriores. Con objeto de habilitar la autenticación mediante token,
ASP.NET Core admite varias opciones para el uso de OAuth 2.0 y OpenID Connect .
C#
// Startup.cs
services.AddAuthentication(options =>
{
options.DefaultScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(setup => setup.ExpireTimeSpan =
TimeSpan.FromMinutes(sessionCookieLifetime))
.AddOpenIdConnect(options =>
{
options.SignInScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = identityUrl.ToString();
options.SignedOutRedirectUri = callBackUrl.ToString();
options.ClientId = useLoadTest ? "mvctest" : "mvc";
options.ClientSecret = "secret";
options.ResponseType = useLoadTest ? "code id_token token" : "code
id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.RequireHttpsMetadata = false;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("orders");
options.Scope.Add("basket");
options.Scope.Add("marketing");
options.Scope.Add("locations");
options.Scope.Add("webshoppingagg");
options.Scope.Add("orders.signalrhub");
});
}
Los recursos de identidad y de API a los que los usuarios podrían solicitar
acceso:
Al especificar los clientes y los recursos que se van a usar en IdentityServer4, puede
pasar una colección IEnumerable<T> del tipo adecuado a los métodos que toman
almacenes de recursos o clientes en memoria. En escenarios más complejos, puede
proporcionar tipos de proveedor de recursos o cliente mediante la inserción de
dependencias.
En el ejemplo siguiente se muestra el aspecto que podría tener una configuración para
que IdentityServer4 use clientes y recursos en memoria proporcionados por un tipo
IClientStore personalizado:
C#
Para este escenario, el middleware de autenticación que controla los tokens JWT está
disponible en el paquete Microsoft.AspNetCore.Authentication.JwtBearer. JWT es el
acrónimo de "JSON Web Token " y es un formato común de token de seguridad
(definido en RFC 7519) para la comunicación de notificaciones de seguridad. Un
ejemplo simplificado de cómo usar el middleware para consumir esos tokens podría ser
similar a este fragmento de código, tomado del microservicio Ordering.Api de
eShopOnContainers.
C#
// Startup.cs
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme =
AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = identityUrl;
options.RequireHttpsMetadata = false;
options.Audience = "orders";
});
}
software intermedio de autenticación del portador de JWT usa este URI para
obtener la clave pública que puede usarse para validar la firma del token. El
middleware también confirma que el parámetro iss del token coincide con este
URI.
Con este software intermedio, los tokens JWT se extraen automáticamente de los
encabezados de autorización. Después, se deserializan, se validan (mediante los valores
de los parámetros Audience y Authority ) y se almacenan como información del usuario
a la que se hará referencia más adelante a través de acciones de MVC o filtros de
autorización.
Recursos adicionales
Uso compartido de cookies entre aplicaciones
https://learn.microsoft.com/aspnet/core/security/cookie-sharing
Introducción a Identity
https://learn.microsoft.com/aspnet/core/security/authentication/identity
Anterior Siguiente
Acerca de la autorización en
microservicios y aplicaciones web de
.NET
Artículo • 29/03/2023
Sugerencia
Descargar PDF
Después de la autenticación, las API web de ASP.NET Core deben autorizar el acceso.
Este proceso permite que un servicio haga que las API estén disponibles para algunos
usuarios autenticados, pero no para todos. La autorización se puede llevar a cabo según
los roles de los usuarios o según una directiva personalizada, que podría incluir la
inspección de notificaciones u otro tipo de heurística.
Restringir el acceso a una ruta de ASP.NET Core MVC es tan fácil como aplicar un
atributo Authorize al método de acción (o a la clase de controlador si todas las acciones
del controlador requieren autorización), como se muestra en el ejemplo siguiente:
C#
[Authorize]
public ActionResult Logout()
{
}
}
C#
[Authorize(Roles = "Administrator")]
public ActionResult ShutDown()
{
}
}
En este ejemplo, solo los usuarios de los roles Administrator o PowerUser pueden tener
acceso a las API del controlador ControlPanel (por ejemplo, para ejecutar la acción
SetTime). La API ShutDown se restringe aún más para permitir el acceso únicamente a
los usuarios con el rol Administrator.
Para pedir a un usuario que tenga varios roles, se usan varios atributos Authorize, como
se muestra en este ejemplo:
C#
C#
services.AddAuthorization(options =>
{
options.AddPolicy("AdministratorsOnly", policy =>
policy.RequireRole("Administrator"));
Como se muestra en el ejemplo, las directivas pueden asociarse con distintos tipos de
requisitos. Una vez registradas las directivas, se pueden aplicar a una acción o a un
controlador pasando el nombre de la directiva como argumento de la directiva del
atributo Authorize (por ejemplo, [Authorize(Policy="EmployeesOnly")] ). Las directivas
pueden tener varios requisitos, no solo uno (como se muestra en estos ejemplos).
La segunda llamada AddPolicy muestra una manera sencilla de pedir que una
notificación concreta esté presente para el usuario. El método RequireClaim también
toma opcionalmente valores esperados para la notificación. Si se especifican valores, el
requisito se cumple solo si el usuario tiene tanto una notificación del tipo correcto como
uno de los valores especificados. Si usa el middleware de autenticación de portador
JWT, todas las propiedades JWT estarán disponibles como notificaciones de usuario.
C#
// Program.cs
builder.Services.AddAuthorizationBuilder()
.AddPolicy("admin_greetings", policy =>
policy
.RequireRole("admin")
.RequireScope("greetings_api"));
Recursos adicionales
ASP.NET Core Authentication (Autenticación en ASP.NET Core)
https://learn.microsoft.com/aspnet/core/security/authentication/identity
Anterior Siguiente
Almacenar secretos de aplicación de
forma segura durante el desarrollo
Artículo • 09/05/2023
Sugerencia
Descargar PDF
Para conectar con los recursos protegidos y otros servicios, las aplicaciones de ASP.NET
Core normalmente necesitan usar cadenas de conexión, contraseñas u otras
credenciales que contienen información confidencial. Estos fragmentos de información
confidenciales se denominan secretos. Es un procedimiento recomendado no incluir
secretos en el código fuente y, ciertamente, no almacenar secretos en el control de
código fuente. En su lugar, debe usar el modelo de configuración de ASP.NET Core para
leer los secretos desde ubicaciones más seguras.
Debe separar los secretos usados para acceder a los recursos de desarrollo y
almacenamiento provisional de los usados para acceder a los recursos de producción, ya
que distintas personas deben tener acceso a los diferentes conjuntos de secretos. Para
almacenar secretos usados durante el desarrollo, los enfoques comunes son almacenar
secretos en variables de entorno o usar la herramienta ASP.NET Core Secret Manager.
Para un almacenamiento más seguro en entornos de producción, los microservicios
pueden almacenar secretos en un Azure Key Vault.
Almacenamiento de secretos en variables de
entorno
Una manera de mantener secretos fuera del código fuente es que los desarrolladores
establezcan secretos basados en cadena como variables de entorno en sus máquinas de
desarrollo. Cuando use variables de entorno para almacenar secretos con nombres
jerárquicos, como las anidadas en las secciones de configuración, debe asignar un
nombre a las variables para incluir la jerarquía completa de sus secciones, delimitada
por signos de dos puntos (:).
JSON
{
"Logging": {
"LogLevel": {
"Default": "Debug"
}
}
}
Para acceder a estos valores desde variables de entorno, la aplicación solo tiene que
llamar a AddEnvironmentVariables en su ConfigurationBuilder al construir un objeto
IConfigurationRoot .
7 Nota
Las variables de entorno suelen almacenarse como texto sin formato, por lo que si
se pone en peligro la máquina o el proceso con las variables de entorno, se verán
los valores de las variables de entorno.
La propiedad UserSecretsId del proyecto que está usando los secretos organiza los
secretos que establece la herramienta Secret Manager. Por tanto, debe asegurarse de
establecer la propiedad UserSecretsId en el archivo del proyecto, como se muestra en el
siguiente fragmento. El valor predeterminado es un GUID asignado por Visual Studio,
pero la cadena real no es importante mientras sea única en su equipo.
XML
<PropertyGroup>
<UserSecretsId>UniqueIdentifyingString</UserSecretsId>
</PropertyGroup>
Para usar los secretos almacenados con Secret Manager en una aplicación, debe llamar
a AddUserSecrets<T> en la instancia de ConfigurationBuilder para incluir los secretos de
la aplicación en su configuración. El parámetro genérico T debe ser un tipo del
ensamblado que se aplicó a UserSecretId. Normalmente, usar AddUserSecrets<Startup>
está bien.
Anterior Siguiente
Usar Azure Key Vault para proteger
secretos en tiempo de producción
Artículo • 09/05/2023
Sugerencia
Descargar PDF
1. Registre la aplicación como una aplicación de Azure AD. (el acceso a los almacenes
de claves se administra mediante Azure AD). Puede hacerlo a través del portal de
administración de Azure.\
PowerShell
) Importante
Recursos adicionales
Using Azure Key Vault to protect application secrets (Usar Azure Key Vault para
proteger secretos de aplicaciones)
https://learn.microsoft.com/azure/architecture/multitenant-identity
Anterior Siguiente
Conclusiones de la arquitectura de
microservicios de .NET
Artículo • 10/05/2023
Sugerencia
Descargar PDF
A modo de resumen y puntos clave, estas son las conclusiones más importantes de esta
guía.
Contenedores para cualquier aplicación. Los contenedores son prácticos para los
microservicios, pero también pueden resultar útiles para las aplicaciones monolíticas
basadas en el .NET Framework tradicional, al usar contenedores de Windows. Las
ventajas de usar Docker, como solucionar muchos problemas relacionados con el paso
de la implementación a la producción y proporcionar entornos de desarrollo y prueba
vanguardistas, se aplican a muchos tipos diferentes de aplicaciones.
CLI frente a IDE. Con herramientas de Microsoft, puede desarrollar aplicaciones .NET en
contenedores con su método preferido. Puede desarrollar con una CLI y un entorno
basado en editor mediante la CLI de Docker y Visual Studio Code. O bien, puede usar un
enfoque centrado en IDE con Visual Studio y sus características exclusivas para Docker,
como la depuración de múltiples contenedores.
Anterior