Comunicação entre Bounded Contexts

Objetivo

Olá tudo bem? Até agora, falei sobre o que é, os motivos pelo quais é uma abordagem que deve estar em nossa “caixa de ferramentas” e como definimos a comunicação entre bounded contexts.

Neste post, veremos como realizamos a comunicação entre BC via mensagens, utilizando o RabbitMQ como nosso message broker.

Message broker

Message broker é um pattern arquitetural que intermédia a troca de mensagens entre aplicações, minimizando o conhecimento que uma aplicação deveria ter sobre a outra, isto é, minimizando o acoplamento entre ambas.

De uma forma bem simples, o broker tem o papel de levar uma informação (mensagem) gerada por uma aplicação para uma outra que está interessada em recebe-la.

RabbitMQ

RabbitMQ é um middleware (ou message-broker software) de mensageira que suporta o protocolo AMQP. Em suma, ele é um software que faz o papel do broker.

Desafio

Para colocarmos nosso conhecimento já adquirido em prática, nosso case será baseado em um e-commerce, onde trabalharemos com dois BC.

Neste nosso e-commerce, toda vez que um cliente realizar um pedido, a área de entrega deve ser notificada que um novo pedido foi feito para que o processo de envio dos produtos adquiridos no pedido se inicie.

Em uma breve analise, podemos identificar dois bounded context, o de pedidos e o de entrega. levando em conta que o nosso BC de pedido fornece a informação de quais pedidos foram feitos e que precisam entrar no processo de envio, podemos ter um context map parecido com a figura a seguir:

OrderAndShippingContextMap
figura 1.

Especificações

Para que o pedido seja feito, se faz obrigatório as seguintes informações:

  • Identificação do usuário que está logado na aplicação
  • a identificação do(s) produto(s) selecionado(s)
  • a quantidade de cada produto
  • O pedido deve possuir uma identificação unica

Quanto o pedido for processado e realizado, o contexto de entrega precisa saber:

  • Qual a identificação do pedido realizado
  • Qual a identificação do usuário que realizou o pedido
  • Qual a quantidade de produtos nos pedido

Com essas informações em mãos, vamos iniciar nosso desenvolvimento

Modelagem tática

Para implementação deste nosso e-commerce, utilizarei a ideia de arquitetura hexagonal e publish/subscriber para troca de mensagem.

Preparando o ambiente

Para utilizar o rabbitmq, você precisar baixar o erlang o rabbitmq server e instalar em suma maquina/servidor. Instale na respectiva ordem.

A Instalação não possui nenhum segredo e ambas são concluídas de forma bem rápida.

Obs: Para se familiarizar e entender melhor como o rabbitmq funciona, sugiro que você realize as 6 etapas de tutoriais disponíveis no site deles.

 Criando o Bounded Context de Pedidos

Agregado de pedido:

using Orders.Core.Domain.OrderAggregate.Scopes;
using System;
using System.Collections.Generic;
using System.Linq;
 
namespace Orders.Core.Domain.OrderAggregate
{
    public sealed class Order
    {
        private IList<OrderItem> _orderItems;
 
        public Guid Id { get; private set; }
        public Guid UserId { get; private set; }
        public DateTime CreationDate { get; private set; }
        public ICollection<OrderItem> Itens { get { return _orderItems; } private set { _orderItems = new List<OrderItem>(value); } }
 
        public Order(IList<OrderItem> orderItems, Guid userId)
        {
 
            Id = Guid.NewGuid();
            UserId = userId;
            _orderItems = new List<OrderItem>();
            orderItems.ToList().ForEach(item => AddItem(item));
            CreationDate = DateTime.Now;
        }
 
        public bool IsValid()
        {
            return this.PlaceAnOrderScope();
        }
 
        private void AddItem(OrderItem item)
        {
            _orderItems.Add(item);
        }
 
        protected Order()
        {
 
        }
    }
}

using System;
 
namespace Orders.Core.Domain.OrderAggregate
{
    public sealed class OrderItem
    {
        public Guid Id { get; private set; }
        public Guid ProductId { get; private set; }
        public int Quantity { get; private set; }
 
        public OrderItem(Guid productId, int quantity)
        {
            Id = Guid.NewGuid();
            ProductId = productId;
            Quantity = quantity;
        }
 
        protected OrderItem()
        {
 
        }
 
    }
}

Agora os testes para garantia de que o pedido é criado da forma correta:

 
using FluentAssertions;
using Orders.Core.Domain.OrderAggregate;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
 
namespace Orders.Core.Tests
{
    public class GivenPlaceAnOrder
    {
 
        Order newOrder;
        List<OrderItem> orderItems;
        public GivenPlaceAnOrder()
        {
            orderItems = new List<OrderItem>();
            orderItems.Add(new OrderItem(Guid.NewGuid(), 1));
            newOrder = new Order(orderItems, Guid.NewGuid());
        }
 
        [Fact]
        public void ThenOrderIsCreated()
        {
            Assert.NotNull(newOrder);
        }
 
        [Fact]
        public void ThenOrderMustHaveAnId()
        {
            newOrder.Id.Should().NotBe(new Guid());
        }
 
        [Fact]
        public void ThenOrderMustHaveAnUserId()
        {
            newOrder.UserId.Should().NotBe(new Guid());
        }
 
        [Fact]
        public void ThenOrderMustHaveAtLeastOneOrderItem()
        {
            newOrder.Itens.Count().Should().BeGreaterThan(0);
        }
 
        [Fact]
        public void ThenOrderItemMustHaveProductId()
        {
            newOrder.Itens.First().ProductId.Should().NotBe(new Guid());
        }
 
        [Fact]
        public void ThenQuantityOfOrderItemMustBeGreaterThanZero()
        {
            newOrder.Itens.First().Quantity.Should().BeGreaterThan(0);
        }
    }
}

Na Application Layer,  encapsulo a criação do pedido e disparo o evento que informa que meu pedido foi criado:

using Orders.Core.ApplicationLayer.Interfaces;
using Orders.Core.ApplicationLayer.Commands;
using System.Collections.Generic;
using SharedKernel.Events;
using SharedKernel.Interfaces;
using Orders.Core.Domain.OrderAggregate;
using Orders.Core.Domain.Events;
using Orders.Core.Domain.Interfaces.Repository;
 
namespace Orders.Core.ApplicationLayer.UseCases
{
    public sealed class OrdersManagement : UseCase, IOrdersManagement
    {
        private readonly IOrderRepository _orderRepository;
        public OrdersManagement(INotifiable<DomainNotification> domainNotification, IEnumerable<IUnitOfWork> uow, IOrderRepository orderRepository)
            : base(domainNotification, uow)
        {
            _orderRepository = orderRepository;
        }
 
        public void PlaceAnOrder(PlaceAnOrderCommand command)
        {
            var itens = new List<OrderItem>();
            foreach (var item in command.Itens)
            {
                var newItem = new OrderItem(item.ProductId, item.Quantity);
                itens.Add(newItem);
            }
            var newOrder = new Order(itens, command.UserId);
            if (newOrder.IsValid())
            {
                _orderRepository.Create(newOrder);
                DomainEvent.Raise(new OrderPlaced(newOrder));
            }
 
            Commit();
        }
 
    }
}

No meu eventHandler de pedido realizado, faço a chamada do publisher, isto é, da classe que irá implementar as configurações do rabbitmq para inserir as informações na fila.

using Orders.Core.Domain.Events;
using SharedKernel.Interfaces;
using System;
 
namespace Orders.Core.ApplicationLayer.Handlers
{
    public class OrderPlacedHandler : IEventHandler<OrderPlaced>
    {
        private readonly IMessagePublisher<OrderPlaced> _messagePublisher;
        public OrderPlacedHandler(IMessagePublisher<OrderPlaced> messagePublisher)
        {
            _messagePublisher = messagePublisher;
    
        }
        public void Dispose()
        {
            throw new NotImplementedException();
        }
 
        public void Handle(OrderPlaced args)
        {
            _messagePublisher.Publish(args);
 
        }
    }
}

A implementação do método Publish, da interface IMessagePublisher é realizada na camada de infraestrutura. É nesta etapa que publicamos a mensagem na fila que fica no rabbitmq server. Para que o código abaixo funcione, é necessário a instalação do RabbitMQ.Client, que pode ser feita via nuget.

using SharedKernel.Interfaces;
using RabbitMQ.Client;
using Orders.Core.Domain.Events;
using Newtonsoft.Json;
using System.Text;
using SharedKernel.Events;
using SharedKernel.Messaging;
using System;
 
namespace Orders.Infra.Publishers
{
    public class OrderPlacedPublisher : IMessagePublisher<OrderPlaced>
    {
 
        public void Publish(OrderPlaced domainEvent)
        {
            var factory = new ConnectionFactory() { HostName = "localhost" };
            using (var connection = factory.CreateConnection())
            {
                using (var channel = connection.CreateModel())
                {
                    channel.ExchangeDeclare(exchange: "OrderPlaced", type: "fanout");
                    var message = JsonConvert.SerializeObject(domainEvent, Formatting.None);
                    var content = new Message { Key = "OrderPlaced", Value = message, DateOccurred = DateTime.Now };
                    var body = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content, Formatting.None));
                    var properties = channel.CreateBasicProperties();
 
 
                    properties.Persistent = true;
                    channel.BasicPublish
                        (
                            exchange: "OrderPlaced",
                            routingKey: string.Empty,
                            basicProperties: properties,
                            body: body
                        );
                }
            }
        }
    }
}

No código acima, nos criamos uma conexão com o rabbitmq server e abrimos um canal que possui um intermediador(exchange) que irá inserir as informações na fila. A informação em si é serializada e enviada para o intermediador publicar.

Quando a mensagem é publicada, ou seja, inserida na fila que fica no rabbitmq sever, todos os interessados nesta publicação são notificados e recebem a mensagem.

Ouvindo as notificações

Até agora, criamos nosso contexto de pedidos e criamos as configurações necessárias para que, toda vez que um pedido for realizado, um evento seja disparado e as informações do pedido sejam publicadas em uma fila, utilizando o rabbitmq.

Da mesma forma que configuramos o nosso publisher, precisamos configurar o nosso listener, para que os interessados em receber essa mensagem sejam notificados.

Para que isso seja possível,  criamos nosso listener e adicionamos ele no global.asax, para que, toda vez que a aplicação for iniciada, a configuração também seja. Para que o código a seguir funcione, também precisamos instalar o RabbitMQ.Client . Veja no código abaixo:


using Newtonsoft.Json;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using SharedKernel.Messaging;
using Shipping.Core.ApplicationLayer.Commands;
using System;
using System.Diagnostics;
using System.Text;
 
namespace WebApi.Helpers
{
    public class MessageListener
    {
        private static IConnection _connection;
        private static IModel _channel;
 
     
 
        public static void Start()
        {
            var factory = new ConnectionFactory()
            {
                HostName = "localhost",
                AutomaticRecoveryEnabled = true,
                NetworkRecoveryInterval = TimeSpan.FromSeconds(15
                )
            };
 
            _connection = factory.CreateConnection();
            _channel = _connection.CreateModel();
            _channel.ExchangeDeclare("OrderPlaced", "fanout");
 
            var queueName = _channel.QueueDeclare().QueueName;
            _channel.QueueBind(queue: queueName, exchange: "OrderPlaced", routingKey: "");
            _channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);
 
            var consumer = new EventingBasicConsumer(_channel);
            var message = string.Empty;
            consumer.Received += ConsumerOnReceived;
            _channel.BasicConsume(queue: queueName, noAck: false, consumer: consumer);
            Debug.WriteLine("listening...");
 
        }
 
 
 
        private static void ConsumerOnReceived(object sender, BasicDeliverEventArgs ea)
        {
            Debug.WriteLine("publish maked");
            var body = ea.Body;
            var message = Encoding.UTF8.GetString(body);
            var notification = JsonConvert.DeserializeObject<Message>(message);
            ProcessMessageContent<NewShippingCommand>.Process(notification);
            _channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
 
        }
 
        public static void Stop()
        {
            _channel.Close(200, "Goodbye");
            _connection.Close();
        }
    }
}

No código acima, criamos uma conexão e abrimos um canal de comunicação com o rabbitmq server. Criamos um intermediador(exchange) e fazemos um binding com o intermediador que queremos ouvir, ou seja, é aqui que definimos de qual fila queremos receber as mensagens que são publicadas.

Todas vez que uma mensagem for publicada, o evento ConsumerOnReceived  será disparado e a mensagem é recebida. Veja com detalhes o código:

private static void ConsumerOnReceived(object sender, BasicDeliverEventArgs ea)
{
    var body = ea.Body;
    var message = Encoding.UTF8.GetString(body);
    var notification = JsonConvert.DeserializeObject<Message>(message);
    ProcessMessageContent<NewShippingCommand>.Process(notification);
    _channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
 
}

No código acima, recebemos a mensagem através da propriedade Body, convertemos ela para o tipo Message e através do método Process da classe ProcessMessageContent enviamos as informações para o contexto de entregas. Veja a implementação da classe no código abaixo

public static class ProcessMessageContent<TCommand> where TCommand : ICommand
{
 
    public static void Process(Message message)
    {
        var context = JsonConvert.DeserializeObject<TCommand>(message.Value);
        DomainCommand.Send<TCommand>(context);
    }
}

No código acima, recebemos as mensagem e desserializamos o conteúdo dela para o tipo especificado na chamada do método. Por fim, através do método Send, enviamos aos interessados em receber. Veja a implementação do método abaixo:



using SharedKernel.Commands.Interfaces;
using SharedKernel.Interfaces;
using System;
namespace SharedKernel.Commands
{
    public static  class DomainCommand
    {
        public static IContainer Container { get; set; }
 
        public static void Send<T>(T args) where T : ICommand
        {
            try
            {
                if (Container != null)
                {
                    var obj = Container.GetService(typeof(IMessageHandler<T>));
                    ((IMessageHandler<T>)obj).Handle(args);
                }
            }
            catch (Exception ex)
            {
                throw new Exception("Error in container commands", ex);
            }
        }
    }
}

Criando o Bounded Context de Entrega

Até agora fizemos bastante coisa, criamos nosso contexto de pedido, configuramos nosso publisher e nosso listener. Agora vamos implementar nosso contexto de entrega para que as mensagens publicadas possam ser recebidas.

A ideia é que, o contexto de entrega, receba as informações do pedido e gere um solicitação de entrega, para que o pedido feito seja enviado para o cliente.

Agregado de entrega:

using System; 
namespace Shipping.Core.Domain.ShippingAggregate
{
    public sealed class Shipping
    {
        public Guid Id { get; private set; }
        public Guid OrderId { get; private set; }
        public Guid UserId { get; set; }
        public DateTime CreationDate { get; private set; }
        public int ItemsQuantity { get; private set; }
 
        public Shipping(Guid orderId, Guid userId, int itemsQuantity)
        {
            Id = Guid.NewGuid();
            OrderId = orderId;
            UserId = userId;
            ItemsQuantity = itemsQuantity;
            CreationDate = DateTime.Now;
        }
 
        protected Shipping()
        {
 
        }
 
    }
}

Os testes para garantir que a criação de uma entrega esteja correta:

using FluentAssertions;
using System;
using Xunit;
 
namespace Shipping.Core.Tests
{
    public class GivenAShipping
    {
        public Domain.ShippingAggregate.Shipping Shipping { get; set; }
    }
 
    public class WhenCreateAShipping:IClassFixture<GivenAShipping>
    {
        private readonly GivenAShipping _fixture;
 
        public WhenCreateAShipping(GivenAShipping fixture)
        {
            _fixture = fixture;
 
            _fixture.Shipping = new Domain.ShippingAggregate.Shipping(Guid.NewGuid(), Guid.NewGuid(),1);
        }
 
        [Fact]
        public void ThenShippingMustHaveId()
        {
            _fixture.Shipping.Id.Should().NotBe(new Guid());       
 
        }
 
        [Fact]
        public void ThenShippingMustHaveUserId()
        {
            _fixture.Shipping.UserId.Should().NotBe(new Guid());
        }
 
        [Fact]
        public void ThenShippingMustHaveOrderId()
        {
            _fixture.Shipping.OrderId.Should().NotBe(new Guid());
 
        }
 
        [Fact]
        public void ThenShippingMustHaveCreationDate()
        {
            _fixture.Shipping.CreationDate.Should().NotBe(new DateTime());
        }
 
        [Fact]
        public void ThenShippingMustHaveItemsQuantity()
        {
            _fixture.Shipping.ItemsQuantity.Should().NotBe(0);
        }
 
    }
}

Criação do handler de novo pedido para entrega:

using SharedKernel.Commands.Interfaces;
using SharedKernel.Events;
using SharedKernel.Interfaces;
using Shipping.Core.ApplicationLayer.Commands;
using Shipping.Core.ApplicationLayer.UseCases;
using Shipping.Core.Domain.Interfaces.Repository;
using System;
using System.Collections.Generic;
 
namespace Shipping.Core.ApplicationLayer.Handlers
{
    public class NewOrderForShippingHandler : UseCase, IMessageHandler<NewShippingCommand>
    {
        private readonly IShippingRepository _shippingRepository;
 
        public NewOrderForShippingHandler
            (IShippingRepository shippingRepository,
            INotifiable<DomainNotification> notification,
            IEnumerable<IUnitOfWork> uow)
            : base(notification, uow)
        {
            _shippingRepository = shippingRepository;
        }
        public void Dispose()
        {
            throw new NotImplementedException();
        }
 
        public void Handle(NewShippingCommand args)
        {
            if (args.IsValid())
            {
                var newShipping =
                    new Domain.ShippingAggregate.Shipping(args.OrderId, args.UserId, args.ItemsQuantity);
                _shippingRepository.Create(newShipping);
                Commit();
            }
        }
    }
}

É esta classe que será chamada quando o nosso método Process for executado. Aqui, esperamos que um newShippingCommand seja recebido através do método Handle. Criamos uma entrega(shipping) e validamos objeto. Caso o retorno seja positivo, persistimos a nova entrega na base de dados.

Veja na imagem a seguir, de uma forma macro, como fica a comunicação entre os bounded context:

Slide1
Figura 2

Conclusão

Aprendemos aqui,  como realizamos a comunicação entre bounded contexts  de uma forma escalável e sem gerar acoplamento entre eles.

Descobrimos também um pouco do poder que um message broker possui e como podemos utilizar a ideia de publish/subscriber em uma aplicação web.

O código completo deste exemplo está no meu github.

A critica de todos são extremamente importantes e bem vindas 🙂

Espero ter ajudado!

 

Referências

https://en.wikipedia.org/wiki/Message_broker

https://en.wikipedia.org/wiki/Advanced_Message_Queuing_Protocol

https://en.wikipedia.org/wiki/RabbitMQ

https://www.erlang.org/

http://www.rabbitmq.com/

http://elemarjr.com/pt/2014/01/03/hexagonal-architecture-ports-and-adapters/

 

Anúncios

6 comentários sobre “Comunicação entre Bounded Contexts

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s