Aplicación de referencia Dapr

Sugerencia

Este contenido es un extracto del libro electrónico "Dapr for .NET Developers" (Dapr para desarrolladores de .NET), disponible en Documentación de .NET o como un PDF descargable gratuito que se puede leer sin conexión.

Miniatura de la portada del libro electrónico

A lo largo de este libro, ha obtenido información sobre las ventajas fundamentales de Dapr. Ha visto cómo Dapr puede ayudarle a usted y a su equipo a construir aplicaciones distribuidas y a reducir la complejidad de la arquitectura y el funcionamiento. A lo largo del proceso, ha tenido la oportunidad de crear algunas aplicaciones pequeñas de Dapr. Ahora es el momento de explorar de qué forma una aplicación más compleja puede beneficiarse de Dapr.

Pero primero veamos un poco de historia.

eShopOnContainers

Hace varios años, Microsoft publicó en colaboración con los principales expertos de la comunidad un libro de instrucciones popular, titulado Microservicios de .NET para aplicaciones .NET contenedorizadas. En la figura 12-1 se muestra el libro:

Microservicios de .NET: diseño de aplicaciones .NET contenedorizadas.

Figura 12-1. Microservicios de .NET: diseño de aplicaciones .NET contenedorizadas.

El libro profundizaba en los principios, patrones y procedimientos recomendados para compilar aplicaciones distribuidas. Incluía una aplicación de referencia de microservicio completa que mostraba los conceptos de arquitectura, denominada eShopOnContainers. Dicha aplicación hospeda un escaparate de comercio electrónico que vende varios artículos, incluida ropa y tazas. La aplicación está compilada en .NET, es multiplataforma y se puede ejecutar en contenedores de Linux o Windows. En la figura 12-2 se muestra la arquitectura original de la tienda electrónica.

Arquitectura de la aplicación de referencia eShopOnContainers.

Figura 12-2. Aplicación de referencia ShopOnContainers original.

Como puede ver, eShopOnContainers incluye muchos elementos móviles:

  1. Tres clientes front-end diferentes.
  2. Una puerta de enlace de aplicaciones para abstraer los servicios back-end del front-end.
  3. Varios microservicios principales de back-end.
  4. Un componente de bus de eventos que permite la mensajería asincrónica de publicación y subscripción.

La aplicación de referencia eShopOnContainers se ha aceptado ampliamente en toda la comunidad de .NET y se usa para modelar muchas aplicaciones de microservicios comerciales de gran tamaño.

eShopOnDapr

Este libro va acompañado de una versión actualizada de la tienda electrónica. Se denomina eShopOnDapr. La actualización desarrolla la aplicación eShopOnContainers anterior mediante la integración de bloques de creación de Dapr. En la figura 12-3 se muestra la nueva arquitectura de la solución:

Arquitectura de la aplicación de referencia eShopOnDapr.

Figura 12-3. Arquitectura de la aplicación de referencia eShopOnDapr.

Aunque eShopOnDapr se centra en Dapr, la arquitectura también se ha simplificado.

  1. Una aplicación de página única que se ejecuta en Blazor WebAssembly envía solicitudes de usuario a una puerta de enlace de API.

  2. La puerta de enlace de API abstrae los microservicios principales de back-end del cliente front-end. Se implementa mediante Envoy, un proxy de servicio de código abierto de alto rendimiento. Envoy enruta las solicitudes entrantes a los microservicios de back-end. La mayoría de las solicitudes son operaciones CRUD simples (por ejemplo, obtener la lista de marcas del catálogo) y se controlan mediante una llamada directa a un microservicio de back-end.

  3. Otras solicitudes son más complejas lógicamente y requieren varias llamadas de microservicio para funcionar juntas. En estos casos, eShopOnDapr implementa un microservicio agregador que organiza un flujo de trabajo entre esos microservicios necesarios para completar la operación.

  4. Los microservicios de back-end principales implementan la funcionalidad necesaria para un almacén de comercio electrónico. Cada uno es independiente de las demás. En función de patrones de descomposición de dominio ampliamente aceptados, cada microservicio aísla una funcionalidad empresarial específica:

    • El servicio de cesta administra la experiencia de la cesta de la compra del cliente.
    • El servicio de catálogo administra los artículos disponibles para la venta.
    • El servicio de identidad administra la autenticación y la identidad.
    • El servicio de pedidos controla todos los aspectos de la realización y administración de pedidos.
    • El servicio de pago realiza transacciones relacionadas con el pago del cliente.
  5. Siguiendo los procedimientos recomendados, cada microservicio mantiene su propio almacenamiento persistente. La aplicación no comparte un único almacén de datos.

  6. Por último, el bus de eventos encapsula los componentes de publicación y suscripción de Dapr. Permite la mensajería asincrónica de publicación y suscripción en los microservicios. Los desarrolladores pueden conectar cualquier componente de agente de mensajes compatible con Dapr.

Aplicación de bloques de creación de Dapr

En eShopOnDapr, los bloques de creación de Dapr reemplazan una gran cantidad de código estructural complejo y propenso a errores.

En la figura 12-4 se muestra la integración de Dapr en la aplicación.

Arquitectura de la aplicación de referencia eShopOnDapr.

Figura 12-4. Integración de Dapr en eShopOnDapr.

En la figura anterior se muestran los bloques de creación de Dapr (representados como cuadros numerados verdes) que cada servicio eShopOnDapr consume.

  1. La puerta de enlace de API y los servicios de agregador de compras web usan el bloque de creación de invocación del servicio para invocar métodos en los servicios de back-end.
  2. Los servicios de back-end se comunican de forma asincrónica mediante el bloque de creación de publicación y suscripción.
  3. El servicio de cesta usa el bloque de creación de administración del estado para almacenar el estado de la cesta de la compra del cliente.
  4. La aplicación eShopOnContainers original muestra conceptos y patrones de DDD (diseño guiado por el dominio) en el servicio de pedidos. eShopOnDapr usa el bloque de creación de actores como una implementación alternativa. El modelo de acceso basado en turnos de los actores facilita la implementación de un proceso de pedidos con estado compatible con la cancelación.
  5. El servicio de pedidos envía correos electrónicos de confirmación de los pedidos mediante el bloque de creación de enlaces.
  6. La administración de secretos se realiza mediante el bloque de creación de secretos.

En las secciones siguientes se proporciona más información sobre cómo se aplican los bloques de creación de Dapr en eShopOnDapr.

Administración de estados

En eShopOnDapr, el servicio de cesta usa el bloque de creación de administración del estado para conservar el contenido de la cesta de la compra del cliente. La arquitectura original de eShopOnContainers usaba una interfaz IBasketRepository para leer y escribir datos para el servicio de cesta. La clase RedisBasketRepository proporcionaba la implementación mediante Redis como almacén de datos subyacente. Para que resulte más sencillo realizar una comparación, se muestra a continuación la implementación original de eShopOnContainers:

public class RedisBasketRepository : IBasketRepository
{
    private readonly ConnectionMultiplexer _redis;
    private readonly IDatabase _database;

    public RedisBasketRepository(ConnectionMultiplexer redis)
    {
        _redis = redis;
        _database = redis.GetDatabase();
    }

    public async Task<CustomerBasket> GetBasketAsync(string customerId)
    {
        var data = await _database.StringGetAsync(customerId);

        if (data.IsNullOrEmpty)
        {
            return null;
        }

        return JsonConvert.DeserializeObject<CustomerBasket>(data);
    }

    // ...
}

Este código usa el paquete NuGet StackExchange.Redis de terceros. Los pasos siguientes son necesarios para cargar la cesta de la compra para un cliente determinado:

  1. Inserte un ConnectionMultiplexer de Redis en el constructor. ConnectionMultiplexer se registra con el marco de inserción de dependencias en el archivo Program.cs.

    services.AddSingleton<ConnectionMultiplexer>(sp =>
    {
        var settings = sp.GetRequiredService<IOptions<BasketSettings>>().Value;
        var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true);
        configuration.ResolveDns = true;
        return ConnectionMultiplexer.Connect(configuration);
    });
    
  2. Use ConnectionMultiplexer para crear una instancia de IDatabase en cada clase de consumo.

  3. Use la instancia de IDatabase para ejecutar una llamada StringGet de Redis mediante el customerId especificado como clave.

  4. Compruebe si los datos se cargan desde Redis; si no es así, se devuelve null.

  5. Deserialice los datos de Redis en un objeto CustomerBasket y devuelva el resultado.

En la aplicación de referencia eShopOnDapr actualizada, una nueva clase DaprBasketRepository reemplaza a la clase RedisBasketRepository:

public class DaprBasketRepository : IBasketRepository
{
    private const string StoreName = "eshop-statestore";

    private readonly DaprClient _daprClient;

    public DaprBasketRepository(DaprClient daprClient)
    {
        _daprClient = daprClient;
    }

    public Task<CustomerBasket> GetBasketAsync(string customerId) =>
        _daprClient.GetStateAsync<CustomerBasket>(StoreName, customerId);

    // ...
}

El código actualizado usa el SDK de .NET de Dapr para leer y escribir datos mediante el bloque de creación de administración del estado. Los nuevos pasos para cargar la cesta de un cliente se simplifican drásticamente:

  1. Inserte DaprClient en el constructor. DaprClient se registra con el marco de inserción de dependencias en el archivo Program.cs`_.
  2. Use el método DaprClient.GetStateAsync para cargar los artículos de la cesta de la compra del cliente desde el almacén de estado configurado y devolver el resultado.

La implementación actualizada sigue usando Redis como almacén de datos subyacente. Pero observe cómo Dapr abstrae las referencias de StackExchange.Redis y la complejidad de la aplicación. La aplicación ya no requiere una dependencia directa de Redis. Lo único que necesita es un archivo de configuración de Dapr:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: eshop-statestore
  namespace: eshop
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: redis:6379
  - name: redisPassword
    secretKeyRef:
      name: redisPassword
auth:
  secretStore: eshop-secretstore

La implementación de Dapr también simplifica el cambio del almacén de datos subyacente. Para cambiar a Azure Table Storage, por ejemplo, solo hay que cambiar el contenido del archivo de configuración. No es necesario realizar ningún cambio en el código.

Invocación de servicio

La aplicación eShopOnContainers original usaba una combinación de servicios gRPC y HTTP/REST. El uso de gRPC se limitaba a la comunicación entre un servicio de agregador y los servicios de back-end principales. En la figura 12-5 se muestra la arquitectura original:

Llamadas HTTP/REST y gRPC en eShopOnContainers.

Figura 12-5. Llamadas HTTP/REST y gRPC en eShopOnContainers.

Observe los pasos de la figura anterior:

  1. El front-end llama a la puerta de enlace de API mediante HTTP/REST.

  2. La puerta de enlace de API reenvía solicitudes CRUD (crear, leer, actualizar y eliminar) simples directamente a un servicio de back-end principal mediante HTTP/REST.

  3. La puerta de enlace de API reenvía solicitudes complejas que implican llamadas de servicio back-end coordinadas al servicio de agregador de compras web.

  4. El servicio de agregador usa gRPC para llamar a los servicios de back-end principales.

En la implementación actualizada de eShopOnDapr, se agregan sidecares de Dapr a los servicios y la puerta de enlace de API. En la figura 12-6 se muestra la arquitectura actualizada:

Llamadas HTTP/REST y gRPC con sidecares en eShopOnContainers.

Figura 12-6. Arquitectura de la tienda electrónica actualizada mediante Dapr.

Observe los pasos actualizados de la figura anterior:

  1. El front-end sigue usando HTTP/REST para llamar a la puerta de enlace de API.

  2. La puerta de enlace de API reenvía las solicitudes HTTP a su sidecar de Dapr.

  3. El sidecar de la puerta de enlace de API envía la solicitud al sidecar del servicio agregador o de back-end.

  4. El servicio agregador usa el SDK de .NET de Dapr para llamar a los servicios de back-end mediante su arquitectura sidecar.

Dapr implementa las llamadas entre los sidecares con gRPC. Por lo tanto, incluso si invoca un servicio remoto con la semántica HTTP/REST, una parte del transporte se implementa mediante gRPC.

La aplicación de referencia eShopOnDapr se beneficia del bloque de creación de invocación del servicio de Dapr. Las ventajas también incluyen la detección de servicios, mTLS automático y observabilidad integrada.

Reenvío de solicitudes HTTP mediante Envoy y Dapr

Tanto la aplicación de tienda electrónica original como la actualizada usan el proxy de Envoy como puerta de enlace de API. Envoy es un proxy de código abierto y bus de comunicación popular entre las aplicaciones distribuidas modernas. Lo creó originariamente Lyft y es propiedad de Cloud Native Computing Foundation, que se encarga de su mantenimiento.

En la implementación original de eShopOnContainers, la puerta de enlace de API de Envoy reenviaba las solicitudes HTTP entrantes directamente a los servicios agregadores o de back-end. En la nueva aplicación eShopOnDapr, el proxy de Envoy reenvía la solicitud a un sidecar de Dapr.

Envoy se configura mediante un archivo de definición YAML para controlar el comportamiento del proxy. Para permitir que Envoy reenvíe las solicitudes HTTP a un contenedor sidecar de Dapr, se agrega un clúster dapr a la configuración. La configuración del clúster contiene un host que apunta al puerto HTTP en el que está escuchando el sidecar de Dapr:

clusters:
- name: dapr
  connect_timeout: 0.25s
  type: strict_dns
  hosts:
  - socket_address:
    address: 127.0.0.1
    port_value: 3500

La configuración de ruta de Envoy se actualiza para reescribir las solicitudes entrantes como llamadas al sidecar de Dapr (fíjese bien en el par clave-valor prefix_rewrite):

- name: "c-short"
  match:
    prefix: "/c/"
  route:
    auto_host_rewrite: true
    prefix_rewrite: "/v1.0/invoke/catalog-api/method/"
    cluster: dapr

Imagine que el cliente de front-end quiere recuperar una lista de artículos del catálogo. La API Catalog proporciona un punto de conexión para obtener los artículos del catálogo:

[Route("api/v1/[controller]")]
[ApiController]
public class CatalogController : ControllerBase
{

    [HttpGet("items/by_page")]
    [ProducesResponseType(typeof(PaginatedItemsViewModel), (int)HttpStatusCode.OK)]
    public async Task<PaginatedItemsViewModel> ItemsAsync(
        [FromQuery] int typeId = -1,
        [FromQuery] int brandId = -1,
        [FromQuery] int pageSize = 10,
        [FromQuery] int pageIndex = 0)
    {
        // ...
    }

En primer lugar, el front-end realiza una llamada HTTP directa a la puerta de enlace de la API de Envoy.

GET http://<api-gateway>/c/api/v1/catalog/items

El proxy de Envoy busca una coincidencia con la ruta, reescribe la solicitud HTTP y la reenvía a la API invoke de su sidecar de Dapr:

GET http://127.0.0.1:3500/v1.0/invoke/catalog-api/method/api/v1/catalog/items

El sidecar controla la detección de servicios y enruta la solicitud al sidecar de la API Catalog. Por último, el sidecar llama a la API Catalog para ejecutar la solicitud, capturar artículos del catálogo y devolver una respuesta:

GET http://localhost/api/v1/catalog/items

Realización de llamadas del servicio agregador mediante el SDK de .NET

La mayoría de las llamadas del front-end de la tienda electrónica son llamadas CRUD sencillas. La puerta de enlace de API las reenvía a un único servicio para su procesamiento. Aun así, algunos escenarios requieren que varios servicios de back-end trabajen juntos para completar una solicitud. En el caso de las llamadas más complejas, el servicio de agregador de compras web actúa como mediador en el flujo de trabajo entre servicios. En la figura 12-7 se muestra la secuencia de procesamiento al agregar un artículo a la cesta de la compra:

Llamada de back-end que requiere varios servicios.

Figura 12-7. Llamada de back-end que requiere varios servicios.

El servicio agregador recupera primero los artículos del catálogo desde la API Catalog. Después, valida la disponibilidad y los precios de los artículos. Por último, el servicio agregador actualiza la cesta de la compra mediante una llamada a la API Basket.

El servicio agregador contiene un BasketController que proporciona un punto de conexión para actualizar la cesta de la compra:

[Route("api/v1/[controller]")]
[Authorize]
[ApiController]
public class BasketController : ControllerBase
{
    private readonly ICatalogService _catalog;
    private readonly IBasketService _basket;

    [HttpPost]
    [HttpPut]
    [ProducesResponseType((int)HttpStatusCode.BadRequest)]
    [ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
    public async Task<ActionResult<BasketData>> UpdateAllBasketAsync(
        [FromBody] UpdateBasketRequest data,
        [FromHeader] string authorization)
    {
        BasketData basket;

        if (data.Items is null || !data.Items.Any())
        {
            basket = new();
        }
        else
        {
            // Get the item details from the catalog API.
            var catalogItems = await _catalog.GetCatalogItemsAsync(
                data.Items.Select(x => x.ProductId));

            if (catalogItems == null)
            {
                return BadRequest(
                    "Catalog items were not available for the specified items in the basket.");
            }

            // Check item availability and prices; store results in basket object.
            basket = CreateValidatedBasket(data.Items, catalogItems);
        }

        // Save the updated shopping basket.
        await _basket.UpdateAsync(basket, authorization.Substring("Bearer ".Length));

        return basket;
    }

    // ...
}

El método UpdateAllBasketAsync obtiene el encabezado Authorization de la solicitud entrante mediante un atributo FromHeader. El encabezado Authorization contiene el token de acceso necesario para llamar a servicios de back-end protegidos.

Después de recibir una solicitud para actualizar la cesta, el servicio agregador llama a la API Catalog para obtener los detalles del artículo. El controlador Basket usa un objeto ICatalogService insertado para realizar esa llamada y comunicarse con la API Catalog. La implementación original de la interfaz usaba gRPC para realizar la llamada. La implementación actualizada usa la invocación del servicio Dapr con compatibilidad con HttpClient:

public class CatalogService : ICatalogService
{
    private readonly HttpClient _httpClient;

    public CatalogService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids)
    {
        var requestUri = $"api/v1/catalog/items/by_ids?ids={string.Join(",", ids)}";

        return _httpClient.GetFromJsonAsync<IEnumerable<CatalogItem>>(requestUri);
    }

    // ...
}

Observe que no se requiere código específico de Dapr para realizar la llamada de invocación del servicio. Toda la comunicación se realiza mediante el objeto HttpClient estándar.

El objeto HttpClient de Dapr está configurado para la clase CatalogService en el inicio del programa:

builder.Services.AddSingleton<ICatalogService, CatalogService>(
    _ => new CatalogService(DaprClient.CreateInvokeHttpClient("catalog-api")));

La otra llamada que realiza el servicio agregador es a la API Basket. Solo permite solicitudes autorizadas. El token de acceso se pasa en un encabezado de solicitud Authorization para garantizar que la llamada se realiza correctamente:

public class BasketService : IBasketService
{
    public Task UpdateAsync(BasketData currentBasket, string accessToken)
    {
        var request = new HttpRequestMessage(HttpMethod.Post, "api/v1/basket")
        {
            Content = JsonContent.Create(currentBasket)
        };
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

        var response = await _httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();
    }

    // ...
}

También en este ejemplo solo se usa la funcionalidad HttpClient estándar para llamar al servicio. Esto permite a los desarrolladores que ya están familiarizados con HttpClient poner en práctica sus conocimientos. Incluso permite que el código HttpClient existente use la invocación del servicio Dapr sin realizar ningún cambio.

Publicación y suscripción

Tanto eShopOnContainers como eShopOnDapr usan el patrón de publicación y suscripción para transmitir eventos de integración entre los microservicios. Los eventos de integración incluyen:

  • Cuando un usuario finaliza la compra de productos incluidos en la cesta de la compra.
  • Cuando se realiza correctamente el pago de un pedido.
  • Cuando el período de gracia de una compra expira.

Nota

Considere un evento de integración como un evento que tiene lugar en varios servicios.

Los eventos de eShopOnContainers se basan en la siguiente interfaz IEventBus:

public interface IEventBus
{
    void Publish(IntegrationEvent integrationEvent);

    void Subscribe<T, THandler>()
        where TEvent : IntegrationEvent
        where THandler : IIntegrationEventHandler<T>;
}

En eShopOnContainers se encuentran implementaciones concretas de esta interfaz para RabbitMQ y Azure Service Bus. Cada implementación incluía una gran cantidad de código estructural personalizado que era difícil de entender y mantener.

La versión más reciente de eShopOnDapr simplifica considerablemente el comportamiento de publicación y suscripción mediante Dapr. Para empezar, la interfaz IEventBus se ha reducido a un único método:

public interface IEventBus
{
    Task PublishAsync(IntegrationEvent integrationEvent);
}

Publicación de eventos

En eShopOnDapr, una sola implementación de DaprEventBus puede admitir cualquier agente de mensajes compatible con Dapr. En el bloque de código siguiente se muestra el método de publicación simplificado. Observe que el método PublishAsync usa el cliente Dapr para publicar un evento:

public class DaprEventBus : IEventBus
{
    private const string DAPR_PUBSUB_NAME = "pubsub";

    private readonly DaprClient _dapr;
    private readonly ILogger _logger;

    public DaprEventBus(DaprClient dapr, ILogger<DaprEventBus> logger)
    {
        _dapr = dapr;
        _logger = logger;
    }

    public async Task PublishAsync(IntegrationEvent integrationEvent)
    {
        var topicName = integrationEvent.GetType().Name;

        _logger.LogInformation(
            "Publishing event {@Event} to {PubsubName}.{TopicName}",
            integrationEvent,
            DAPR_PUBSUB_NAME,
            topicName);

        // We need to make sure that we pass the concrete type to PublishEventAsync,
        // which can be accomplished by casting the event to dynamic. This ensures
        // that all event fields are properly serialized.
        await _dapr.PublishEventAsync(DAPR_PUBSUB_NAME, topicName, (object)integrationEvent);
    }
}

Como puede ver en el fragmento de código, el nombre del tema deriva del nombre del tipo de evento. Dado que todos los servicios de la tienda electrónica usan la abstracción IEventBus, el reajuste de Dapr no requiere ningún cambio en el código de la aplicación principal.

Importante

El SDK de Dapr usa System.Text.Json para serializar o deserializar los mensajes. Aun así, System.Text.Json no serializa las propiedades de las clases derivadas de forma predeterminada. En el código de la tienda electrónica, a veces un evento se declara explícitamente como IntegrationEvent, que es la clase base para los eventos de integración. Esta construcción permite determinar dinámicamente el tipo de evento concreto en tiempo de ejecución en función de la lógica de negocios. Como resultado, el evento se serializa mediante la información de tipo de la clase base, y no de la clase derivada. Para forzar que System.Text.Json serialice las propiedades de la clase base y la derivada, el código usa object como parámetro de tipo genérico. Para obtener más información, consulte la documentación de .NET.

Con Dapr, el código de infraestructura de publicación y suscripción se simplifica considerablemente. La aplicación no necesita distinguir entre los agentes de mensajes. Dapr proporciona esta abstracción automáticamente. Si es necesario, puede intercambiar fácilmente agentes de mensajes o configurar varios componentes de agente de mensajes sin realizar cambios en el código.

Suscripción a los eventos

La anterior aplicación eShopOnContainers contiene SubscriptionManagers para controlar la implementación de la suscripción para cada agente de mensajes. Cada administrador contiene código complejo específico del agente de mensajes para controlar los eventos de suscripción. Con el fin de recibir eventos, cada servicio debe registrar explícitamente un controlador para cada tipo de evento.

eShopOnDapr simplifica el código estructural de las suscripciones de eventos mediante la integración de ASP.NET Core en Dapr. Cada evento se controla mediante un método de acción en un controlador. Un atributo Topic decora el método de acción con el nombre del tema correspondiente. Este es un fragmento de código tomado de PaymentService:

[Route("api/v1/[controller]")]
[ApiController]
public class IntegrationEventController : ControllerBase
{
    private const string DAPR_PUBSUB_NAME = "pubsub";

    [HttpPost("OrderStatusChangedToValidated")]
    [Topic(DAPR_PUBSUB_NAME, nameof(OrderStatusChangedToValidatedIntegrationEvent))]
    public Task HandleAsync(
        OrderStatusChangedToValidatedIntegrationEvent integrationEvent,
        [FromServices] OrderStatusChangedToValidatedIntegrationEventHandler handler) =>
        handler.Handle(integrationEvent);
}

En el atributo Topic, el nombre del tipo .NET del evento se usa como nombre del tema. Para controlar el evento, se resuelve mediante la inserción de dependencias un controlador de eventos que ya existía en la base de código anterior de eShopOnContainers y se invoca. En el ejemplo anterior, los mensajes que se reciben del tema OrderStatusChangedToValidatedIntegrationEvent invocan al controlador de eventos OrderStatusChangedToValidatedIntegrationEventHandler existente. Dado que Dapr implementa el código estructural subyacente para las suscripciones y los agentes de mensajes, una gran cantidad del código original quedó obsoleta y se quitó de la base de código. Gran parte de este código era difícil de entender y mantener.

Uso de componentes de publicación y suscripción

Dentro del repositorio de eShopOnDapr, hay una carpeta deployment que contiene archivos para implementar la aplicación mediante diferentes modos de implementación: Docker Compose y Kubernetes. Dentro de cada una de estas carpetas, existe una carpeta dapr que contiene una carpeta components. Esta carpeta contiene un archivo eshop-pubsub.yaml que especifica el componente de publicación y suscripción de Dapr que la aplicación usará para el comportamiento de publicación y suscripción. Como vio en los fragmentos de código anteriores, el nombre del componente de publicación y suscripción que se usa es pubsub. Este es el contenido del archivo eshop-pubsub.yaml de la carpeta deployment/compose/dapr/components:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: pubsub
  namespace: eshop
spec:
  type: pubsub.rabbitmq
  version: v1
  metadata:
  - name: host
    value: "amqp://rabbitmq:5672"

La configuración especifica RabbitMQ como infraestructura subyacente. Para cambiar los agentes de mensajes, solo debe configurar otro agente de mensajes, como NATS o Azure Service Bus, y actualizar el archivo YAML. Con Dapr, no se realiza ningún cambio en el código del servicio principal cuando se cambian los agentes de mensajes.

También puede usar fácilmente varios agentes de mensajes en una sola aplicación. Muchas veces, un sistema controlará cargas de trabajo con características diferentes. Un evento podría producirse 10 veces al día y otro, 5000 veces por segundo. Para aprovechar esta ventaja, cree particiones del tráfico de mensajería a diferentes agentes de mensajes. Con Dapr, puede agregar varias configuraciones del componente de publicación y suscripción, cada una con un nombre diferente.

Enlaces

eShopOnDapr usa el bloque de creación de enlaces para enviar correos electrónicos. Cuando un usuario realiza un pedido, la aplicación envía un correo electrónico de confirmación del pedido mediante el enlace de salida SMTP. Encontrará este enlace en el archivo eshop-email.yaml en la carpeta components:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: sendmail
  namespace: eshop
spec:
  type: bindings.smtp
  version: v1
  metadata:
  - name: host
    value: maildev
  - name: port
    value: 25
  - name: user
    secretKeyRef:
      name: Smtp.User
      key: Smtp.User
  - name: password
    secretKeyRef:
      name: Smtp.Password
      key: Smtp.Password
  - name: skipTLSVerify
    value: true
auth:
  secretStore: eshop-secretstore
scopes:
- ordering-api

Dapr obtiene el nombre de usuario y la contraseña para conectarse al servidor SMTP desde una referencia secreta. Este enfoque guarda los secretos fuera del archivo de configuración. Para obtener más información sobre los secretos de Dapr, lea el capítulo sobre el bloque de creación de secretos.

La configuración del enlace especifica un componente de enlace que se puede invocar mediante el punto de conexión /sendmail en el sidecar de Dapr. Este es un fragmento de código en el que se envía un correo electrónico cada vez que se inicia un pedido:

public Task Handle(OrderStartedDomainEvent notification, CancellationToken cancellationToken)
{
    var message = CreateEmailBody(notification);
    var metadata = new Dictionary<string, string>
    {
        ["emailFrom"] = "eShopOn@dapr.io",
        ["emailTo" = notification.UserName,
        ["subject"] = $"Your eShopOnDapr order #{notification.Order.Id}"
    };
    return _daprClient.InvokeBindingAsync("sendmail", "create", message, metadata, cancellationToken);
}


public Task SendOrderConfirmationAsync(Order order)
{
    var message = CreateEmailBody(order);

    return _daprClient.InvokeBindingAsync(
        "sendmail",
        "create",
        CreateEmailBody(order),
        new Dictionary<string, string>
        {
            ["emailFrom"] = "eshopondapr@example.com",
            ["emailTo"] = order.BuyerEmail,
            ["subject"] = $"Your eShopOnDapr Order #{order.OrderNumber}"
        });
}

Como puede ver en este ejemplo, message contiene el cuerpo del mensaje. El método CreateEmailBody simplemente da formato a una cadena con el texto del cuerpo. El nombre del enlace que se va a invocar es sendmail y la operación es create. metadata especifica el remitente, el destinatario y el asunto del mensaje de correo electrónico. Si estos valores son estáticos, también se pueden incluir en los campos de metadatos del archivo de configuración.

Actores

En la solución original eShopOnContainers, el servicio de pedidos proporciona un buen ejemplo de cómo usar patrones de DDD (diseño guiado por el dominio) en un microservicio de .NET. Como la aplicación eShopOnDapr actualizada se centra en Dapr, el servicio de pedidos ahora usa el bloque de creación de actores para implementar su lógica de negocios.

El proceso de pedidos consta de los pasos siguientes:

  1. El cliente envía el pedido. Hay un período de gracia antes de que se lleve a cabo cualquier otro tipo de procesamiento. Durante el período de gracia, el cliente puede cancelar el pedido.
  2. El sistema comprueba que hay existencias disponibles.
  3. El sistema procesa el pago.
  4. El sistema envía el pedido.

El proceso se implementa mediante un solo tipo de actor OrderingProcessActor. Esta es la interfaz del actor:

public interface IOrderingProcessActor : IActor
{
    Task SubmitAsync(
        string userId, string userName, string street, string city,
        string zipCode, string state, string country, CustomerBasket basket);

    Task NotifyStockConfirmedAsync();

    Task NotifyStockRejectedAsync(List<int> rejectedProductIds);

    Task NotifyPaymentSucceededAsync();

    Task NotifyPaymentFailedAsync();

    Task<bool> CancelAsync();

    Task<bool> ShipAsync();

    Task<Order> GetOrderDetailsAsync();
}

El proceso se inicia cuando un cliente finaliza la compra de algunos productos. Tras la finalización de la compra, el servicio de cesta publica un mensaje UserCheckoutAcceptedIntegrationEvent mediante el bloque de creación de publicación y suscripción de Dapr. El servicio de pedidos controla el mensaje de la clase OrderingProcessEventController y llama al método SubmitAsync del actor:

[HttpPost("UserCheckoutAccepted")]
[Topic(DaprPubSubName, "UserCheckoutAcceptedIntegrationEvent")]
public async Task HandleAsync(UserCheckoutAcceptedIntegrationEvent integrationEvent)
{
    if (integrationEvent.RequestId != Guid.Empty)
    {
        var actorId = new ActorId(integrationEvent.RequestId.ToString());
        var orderingProcess = _actorProxyFactory.CreateActorProxy<IOrderingProcessActor>(
            actorId,
            nameof(OrderingProcessActor));

        await orderingProcess.SubmitAsync(integrationEvent.UserId, integrationEvent.UserName,
            integrationEvent.Street, integrationEvent.City, integrationEvent.ZipCode,
            integrationEvent.State, integrationEvent.Country, integrationEvent.Basket);
    }
    else
    {
        _logger.LogWarning(
            "Invalid IntegrationEvent - RequestId is missing - {@IntegrationEvent}",
            integrationEvent);
    }
}

En el ejemplo anterior, el servicio de pedidos usa primero el identificador de la solicitud original del mensaje UserCheckoutAcceptedIntegrationEvent como identificador de actor. El controlador usa ActorId para crear un proxy de actor e invoca el método SubmitAsync. En el fragmento de código siguiente se muestra la implementación del método SubmitAsync:

public async Task SubmitAsync(
    string buyerId,
    string buyerEmail,
    string street,
    string city,
    string state,
    string country,
    CustomerBasket basket)
{
    var orderState = new OrderState
    {
        OrderDate = DateTime.UtcNow,
        OrderStatus = OrderStatus.Submitted,
        Description = "Submitted",
        Address = new OrderAddressState
        {
            Street = street,
            City = city,
            State = state,
            Country = country
        },
        BuyerId = buyerId,
        BuyerEmail = buyerEmail,
        OrderItems = basket.Items
            .Select(item => new OrderItemState
            {
                ProductId = item.ProductId,
                ProductName = item.ProductName,
                UnitPrice = item.UnitPrice,
                Units = item.Quantity,
                PictureFileName = item.PictureFileName
            })
            .ToList()
    };

    await StateManager.SetStateAsync(OrderDetailsStateName, orderState);
    await StateManager.SetStateAsync(OrderStatusStateName, OrderStatus.Submitted);

    await RegisterReminderAsync(
        GracePeriodElapsedReminder,
        null,
        TimeSpan.FromSeconds(_settings.Value.GracePeriodTime),
        TimeSpan.FromMilliseconds(-1));

    await _eventBus.PublishAsync(new OrderStatusChangedToSubmittedIntegrationEvent(
        OrderId,
        OrderStatus.Submitted.Name,
        buyerId,
        buyerEmail));
}

Suceden muchas cosas en el método Submit:

  1. El método toma los argumentos especificados para crear un objeto OrderState y lo guarda en el estado del actor.
  2. El método guarda el estado actual del proceso (OrderStatus.Submitted) en el estado del actor.
  3. El método registra un recordatorio para indicar el final del período de gracia. El procesamiento del pedido se retrasa hasta el final del período de gracia para tratar los casos de clientes que cambian de opinión.
  4. Por último, el método publica OrderStatusChangedToSubmittedIntegrationEvent para notificar a otros servicios el cambio del estado.

Cuando se activa el recordatorio de la finalización del período de gracia, el runtime del actor llama al método ReceiveReminderAsync:

public Task ReceiveReminderAsync(
    string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period)
{
    return reminderName switch
    {
        GracePeriodElapsedReminder => OnGracePeriodElapsedAsync(),
        StockConfirmedReminder => OnStockConfirmedSimulatedWorkDoneAsync(),
        StockRejectedReminder => OnStockRejectedSimulatedWorkDoneAsync(
            JsonConvert.DeserializeObject<List<int>>(Encoding.UTF8.GetString(state))),
        PaymentSucceededReminder => OnPaymentSucceededSimulatedWorkDoneAsync(),
        PaymentFailedReminder => OnPaymentFailedSimulatedWorkDoneAsync(),
        _ => Task.CompletedTask
    };
}

Como se muestra en el fragmento de código anterior, el método ReceiveReminderAsync no solo controla el recordatorio del período de gracia. El actor también usa recordatorios para simular el trabajo en segundo plano e introducir algunos retrasos en el proceso de pedidos. Esto hace que sea más fácil seguir el proceso en la interfaz de usuario de eShopOnDapr, donde se muestran notificaciones para cada actualización del estado. El método ReceiveReminderAsync usa el nombre del recordatorio para determinar qué método controla el recordatorio. El método OnGracePeriodElapsedAsync controla el recordatorio del período de gracia:

public async Task OnGracePeriodElapsedAsync()
{
    var statusChanged = await TryUpdateOrderStatusAsync(
        OrderStatus.Submitted, OrderStatus.AwaitingStockValidation);
    if (statusChanged)
    {
        var order = await StateManager.GetStateAsync<Order>(OrderDetailsStateName);

        await _eventBus.PublishAsync(new OrderStatusChangedToAwaitingStockValidationIntegrationEvent(
            OrderId,
            OrderStatus.AwaitingStockValidation.Name,
            "Grace period elapsed; waiting for stock validation.",
            order.UserName,
            order.OrderItems
                .Select(orderItem => new OrderStockItem(orderItem.ProductId, orderItem.Units))));
    }
}

Primero, el método OnGracePeriodElapsedAsync intenta actualizar el estado del pedido al nuevo estado AwaitingStockValidation. Si esto se realiza correctamente, recupera los detalles del pedido desde el estado y publica OrderStatusChangedToAwaitingStockValidationIntegrationEvent para informar a otro servicio del cambio del estado. Por ejemplo, el servicio Category se suscribe a este evento para comprobar las existencias disponibles.

Echemos un vistazo al método TryUpdateOrderStatusAsync para ver en qué circunstancias podría no actualizar el estado del pedido:

private async Task<bool> TryUpdateOrderStatusAsync(OrderStatus expectedOrderStatus, OrderStatus newOrderStatus)
{
    var orderStatus = await StateManager.TryGetStateAsync<OrderStatus>(OrderStatusStateName);
    if (!orderStatus.HasValue)
    {
        _logger.LogWarning(
            "Order with Id: {OrderId} cannot be updated because it doesn't exist",
            OrderId);

        return false;
    }

    if (orderStatus.Value.Id != expectedOrderStatus.Id)
    {
        _logger.LogWarning(
            "Order with Id: {OrderId} is in status {Status} instead of expected status {ExpectedStatus}",
            OrderId, orderStatus.Value.Name, expectedOrderStatus.Name);

        return false;
    }

    await StateManager.SetStateAsync(OrderStatusStateName, newOrderStatus);

    return true;
}

En primer lugar, el método TryUpdateOrderStatusAsync comprueba si hay un estado de pedido actual. Si no lo hay, el pedido no existe. Se trata de una notificación de error que no debería producirse en el caso de un uso normal de la aplicación. Después, el método comprueba si el estado del pedido actual es el esperado. Recuerde que el proceso de pedidos se controla mediante eventos con el bloque de creación de publicación y suscripción de Dapr. La entrega de eventos usa la semántica de tipo "una vez como mínimo", por lo que un mismo mensaje podría recibirse varias veces. La comprobación del estado del pedido garantiza que, incluso si el mismo mensaje se recibe varias veces, solo se procesa una vez.

Los demás pasos del proceso de pedidos se implementan de forma muy similar al paso del período de gracia. En las secciones siguientes, veremos otros aspectos del proceso de pedidos, concretamente la cancelación y la visualización de los detalles del pedido.

Cancelación del pedido

Los clientes pueden cancelar cualquier pedido que aún no se haya pagado o enviado. La clase OrdersController controla las cancelaciones de pedidos entrantes. Invoca el método CancelAsync en la instancia de OrderingProcessActor del pedido especificado.

public async Task<bool> CancelAsync()
{
    var orderStatus = await StateManager.TryGetStateAsync<OrderStatus>(OrderStatusStateName);
    if (!orderStatus.HasValue)
    {
        _logger.LogWarning(
           "Order with Id: {OrderId} cannot be cancelled because it doesn't exist",
            OrderId);

        return false;
    }

    if (orderStatus.Value.Id == OrderStatus.Paid.Id || orderStatus.Value.Id == OrderStatus.Shipped.Id)
    {
        _logger.LogWarning(
           "Order with Id: {OrderId} cannot be cancelled because it's in status {Status}",
            OrderId, orderStatus.Value.Name);

        return false;
    }

    await StateManager.SetStateAsync(OrderStatusStateName, OrderStatus.Cancelled);

    var order = await StateManager.GetStateAsync<Order>(OrderDetailsStateName);

    await _eventBus.PublishAsync(new OrderStatusChangedToCancelledIntegrationEvent(
        OrderId,
        OrderStatus.Cancelled.Name,
        $"The order was cancelled by buyer.",
        order.UserName));

    return true;
}

El método CancelAsync consta de los pasos siguientes:

  1. En primer lugar, el método recupera el estado del pedido actual para garantizar que el pedido existe.
  2. Si el pedido existe, el método comprueba si es apto para la cancelación. Los pedidos que no tengan el estado Paid o Shipped pueden cancelarse.
  3. Si el pedido puede cancelarse, su estado cambia a Cancelled.
  4. Por último, los detalles del pedido se recuperan del estado y se usan para publicar un evento OrderStatusChangedToCancelledIntegrationEvent con el fin de informar a los demás servicios.

El método CancelAsync es un excelente ejemplo de la utilidad del modelo de acceso basado en turnos de los actores. No hay que preocuparse en ningún lugar del método por la ejecución de varios subprocesos al mismo tiempo. Por lo tanto, el método no requiere ningún mecanismo de bloqueo explícito para ser correcto.

Detalles de pedido

Los clientes pueden comprobar el estado y los detalles de su pedido en la interfaz de usuario de eShopOnDapr. También pueden ver el historial completo de los pedidos anteriores. No es recomendable consultar directamente las instancias de actor en busca de esta información por dos motivos:

  1. No se pueden garantizar las lecturas de baja latencia porque las operaciones de actor se ejecutan en serie.
  2. Es ineficaz realizar consultas en los actores porque el estado de cada actor debe leerse individualmente y esto puede introducir latencias más impredecibles.

Para corregir este problema, eShopOnDapr usa un modelo de lectura independiente para las consultas que se realicen en los datos de los pedidos. El modelo de lectura se almacena en una base de datos SQL independiente. Una clase de controlador de ASP.NET Core denominada UpdateOrderStatusEventController se suscribe a los eventos de estado del pedido y crea el modelo de vista. La misma clase UpdateOrderStatusEventController también envía notificaciones de inserción a la interfaz de usuario para informar al cliente de las actualizaciones de estado del pedido.

En el fragmento de código siguiente se muestra el código para controlar el mensaje OrderStatusChangedToSubmittedIntegrationEvent:

[HttpPost("OrderStatusChangedToSubmitted")]
[Topic(DaprPubSubName, nameof(OrderStatusChangedToSubmittedIntegrationEvent))]
public async Task HandleAsync(
    OrderStatusChangedToSubmittedIntegrationEvent integrationEvent,
    [FromServices] IOptions<OrderingSettings> settings,
    [FromServices] IEmailService emailService)
{
    // Gets the order details from Actor state.
    var actorId = new ActorId(integrationEvent.OrderId.ToString());
    var orderingProcess = _actorProxyFactory.CreateActorProxy<IOrderingProcessActor>(
        actorId,
        nameof(OrderingProcessActor));
    //
    var actorOrder = await orderingProcess.GetOrderDetailsAsync();
    var readModelOrder = new Order(integrationEvent.OrderId, actorOrder);

    // Add the order to the read model so it can be queried from the API.
    // It may already exist if this event has been handled before (at-least-once semantics).
    readModelOrder = await _orderRepository.AddOrGetOrderAsync(readModelOrder);

    // Send a SignalR notification to the client.
    await SendNotificationAsync(readModelOrder.OrderNumber, integrationEvent.OrderStatus,
        integrationEvent.BuyerId);

    // Send a confirmation e-mail if enabled.
    if (settings.Value.SendConfirmationEmail)
    {
        await emailService.SendOrderConfirmationAsync(readModelOrder);
    }
}

El controlador contiene el código de todas las acciones que deben producirse después de que se envíe correctamente un pedido. Dado que los eventos se originan en OrderingProcessActor, podemos tener la seguridad de que las validaciones realizadas por el actor se han realizado correctamente.

El controlador realiza los pasos siguientes:

  1. En primer lugar, el método crea un proxy de actor y lo usa para recuperar los detalles del pedido de la instancia de actor.
  2. El método asigna los detalles del pedido al modelo de lectura y lo almacena en la base de datos. Debido a la semántica de tipo "una vez como mínimo" del bloque de creación de publicación y suscripción de Dapr, es posible que el pedido ya exista en la base de datos. En ese caso, no se sobrescribirá.
  3. El método publica una notificación de inserción para la actualización del estado mediante SignalR.
  4. Por último, si está habilitado, el método envía un correo electrónico de confirmación al cliente.

Las actualizaciones posteriores del estado del pedido se controlan todas del mismo modo. En el fragmento de código siguiente se muestra lo que sucede cuando el estado del pedido se actualiza a AwaitingStockValidation:

[HttpPost("OrderStatusChangedToAwaitingStockValidation")]
[Topic(DaprPubSubName, nameof(OrderStatusChangedToAwaitingStockValidationIntegrationEvent))]
public Task HandleAsync(
    OrderStatusChangedToAwaitingStockValidationIntegrationEvent integrationEvent)
{
    // Save the updated status in the read model and notify the client via SignalR.
    return UpdateReadModelAndSendNotificationAsync(integrationEvent.OrderId,
        integrationEvent.OrderStatus, integrationEvent.Description, integrationEvent.BuyerId);
}

private async Task UpdateReadModelAndSendNotificationAsync(
    Guid orderId, string orderStatus, string description, string buyerId)
{
    var order = await _orderRepository.GetOrderByIdAsync(orderId);
    if (order is not null)
    {
        order.OrderStatus = orderStatus;
        order.Description = description;

        await _orderRepository.UpdateOrderAsync(order);
        await SendNotificationAsync(order.OrderNumber, orderStatus, buyerId);
    }
}

En el fragmento de código, el controlador llama al método auxiliar UpdateReadModelAndSendNotificationAsync para controlar la actualización del estado:

  1. El método auxiliar carga primero el pedido actual de la base de datos.
  2. Si esto se realiza correctamente, actualiza los campos OrderStatus y Description y guarda el modelo actualizado de nuevo en la base de datos.
  3. Por último, envía una notificación de inserción a la interfaz de usuario del cliente.

Observabilidad

eShopOnDapr usa Zipkin para visualizar los seguimientos distribuidos que recopila Dapr. Seq agrega los registros de la aplicación eShopOnDapr. Los distintos servicios emiten un registro estructurado mediante la biblioteca de registro Serilog. Serilog publica eventos de registro en una construcción denominada receptor. Un receptor es simplemente una plataforma de destino en la que Serilog escribe sus eventos de registro. Hay disponibles muchos receptores de Serilog, incluido uno para Seq. Seq es el receptor de Serilog que se usa en eShopOnDapr.

eShopOnDapr también incluye un panel de mantenimiento personalizado que proporciona información sobre el estado de los servicios de tienda electrónica. Este panel usa el mecanismo integrado de comprobaciones de estado de ASP.NET Core. El panel no solo proporciona el estado de mantenimiento de los servicios, sino también el estado de las dependencias de los servicios, incluidos los sidecares de Dapr.

Secretos

La aplicación de referencia eShopOnDapr usa el bloque de creación de secretos para varios secretos:

  • La contraseña para conectarse a la caché de Redis.
  • El nombre de usuario y la contraseña del servidor SMTP.
  • Las cadenas de conexión para las bases de datos SQL.

Al ejecutar la aplicación mediante Docker Compose, se usa el almacén de secretos del archivo local. El archivo de configuración de componentes eshop-secretstore.yaml se encuentra en la carpeta dapr/components del repositorio eShopOnDapr:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: eshop-secretstore
  namespace: eshop
spec:
  type: secretstores.local.file
  version: v1
  metadata:
  - name: secretsFile
    value: ./components/eshop-secretstore.json
  - name: nestedSeparator
    value: "."

El archivo de configuración hace referencia al archivo del almacén local eshop-secretstore.json ubicado en la misma carpeta:

{
  "ConnectionStrings": {
    "CatalogDB": "**********",
    "IdentityDB": "**********",
    "OrderingDB": "**********"
  },
  "Smtp": {
    "User": "**********",
    "Password": "**********"
  },
  "State": {
    "RedisPassword": "**********"
  }
}

La carpeta components se especifica en la línea de comandos y se monta como una carpeta local dentro del contenedor sidecar de Dapr. Este es un fragmento de código del archivo docker-compose.override.yml en la raíz del repositorio que especifica el montaje del volumen:

catalog-api-dapr:
  command: ["./daprd",
    "-app-id", "catalog-api",
    "-app-port", "80",
    "-components-path", "/components",
    "-config", "/configuration/eshop-config.yaml"
    ]
  volumes:
    - "./dapr/components/:/components"
    - "./dapr/configuration/:/configuration"

El montaje del volumen /components y el argumento de la línea de comandos --components-path se pasan al comando de inicio daprd.

Una vez que se haya configurado, otros archivos de configuración de componentes también pueden hacer referencia a los secretos. Este es un ejemplo de la configuración del componente de almacén de estado que consume secretos:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: eshop-statestore
  namespace: eshop
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: redis:6379
  - name: redisPassword
    secretKeyRef:
      name: State.RedisPassword
      key: State.RedisPassword
  - name: actorStateStore
    value: "true"
auth:
  secretStore: eshop-secretstore
scopes:
- basket-api
- ordering-api

Ventajas de aplicar Dapr a la tienda electrónica

En general, el uso de bloques de creación de Dapr agrega observabilidad y flexibilidad a la aplicación:

  1. Observabilidad: mediante el uso de los bloques de creación de Dapr, se obtiene un seguimiento distribuido enriquecido de las llamadas entre los servicios y a los componentes de Dapr sin necesidad de escribir código. En eShopOnContainers, se usa una gran cantidad de registros personalizados para proporcionar información.
  2. Flexibilidad: ahora puede intercambiar la infraestructura con solo cambiar un archivo de configuración de componentes. No es necesario realizar ningún cambio en el código.

Estos son más ejemplos de las ventajas que ofrecen otros bloques de creación:

  • Invocación del servicio

    • Debido a que Dapr es compatible con mTLS, los servicios ahora se comunican a través de canales cifrados.
    • Cuando se producen errores transitorios, las llamadas de servicio se reintentan automáticamente.
    • La detección automática de servicios reduce el trabajo de configuración necesario para que los servicios se encuentren entre sí.
  • Publicación y suscripción

    • eShopOnContainers incluía una gran cantidad de código personalizado para admitir tanto Azure Service Bus como RabbitMQ. Los desarrolladores usaban Azure Service Bus para la producción y RabbitMQ para el desarrollo y las pruebas locales. Se creó una capa de abstracción de IEventBus para permitir el intercambio entre estos agentes de mensajes. Esta capa constaba aproximadamente de 700 líneas de código propenso a errores. La implementación actualizada con Dapr solo requiere 35 líneas de código, es decir, el 5 % de las líneas de código originales. Y, lo que es más importante, la implementación es sencilla y fácil de entender.
    • eShopOnDapr recurre a la integración enriquecida de ASP.NET Core de Dapr para usar la publicación y suscripción. Para suscribirse a los mensajes, se agregan atributos Topic a los métodos de controlador de ASP.NET Core. Por lo tanto, no es necesario escribir un bucle de controlador de mensajes independiente para cada agente de mensajes.
    • Los mensajes enrutados al servicio como llamadas HTTP permiten el uso de middleware de ASP.NET Core para agregar funcionalidad, sin necesidad de familiarizarse con conceptos o SDK nuevos.
  • Enlaces

    • La solución eShopOnContainers contenía una tarea pendiente para enviar por correo electrónico una confirmación del pedido al cliente. Con Dapr, la implementación de la notificación por correo electrónico es tan fácil como configurar un enlace de recursos.
  • Actores

    • El bloque de creación de actores facilita la creación de flujos de trabajo con estado y de larga duración. Gracias al modelo de acceso basado en turnos, no se requieren mecanismos de bloqueo explícitos.
    • La complejidad de la implementación del período de gracia se reduce considerablemente, ya que se usan recordatorios para los actores, en lugar de sondear la base de datos.

Resumen

En este capítulo, le hemos presentado la aplicación de referencia eShopOnDapr. Se trata de una evolución de la aplicación de referencia de microservicios eShopOnContainers, enormemente popular. eShopOnDapr reemplaza una gran cantidad de funcionalidades personalizadas por componentes y bloques de creación de Dapr, lo que simplifica drásticamente la complejidad necesaria para crear una aplicación de microservicios.

Referencias