Eventos del dominio

Eventos del dominio o Domain Events: Para una correcta utilización, publicación y consumo, enfocada en la paquetización y reutilización sin acoplamientos entre contextos, os propongo el siguiente diseño.

Eventos del dominio

Eventos del dominio o Domain Events

Para una correcta utilización, publicación y consumo, enfocada en la paquetización y reutilización sin acoplamientos entre contextos, os propongo el siguiente diseño.

Contexto aplicado a un ejemplo

Vamos a partir de la base de una tarea en concreto. Esa tarea es la baja del usuario. Supongamos lo siguiente:

  • Usando una arquitectura CQRS, el comando de baja de usuario emitirá un evento del dominio para notificar al mundo de lo que acaba de suceder.
  • Dentro del propio contexto de los usuarios, como algo añadido a la operación de baja, es necesario enviar un email de confirmación de baja al usuario.
  • Dentro del contexto de atención al cliente, es necesario marcar al contacto asociado al usuario como que no quiere comunicaciones comerciales.

Contexto de Usuarios

Proyectos resultantes dentro del contexto de los usuarios, que es quien tiene la operación:

DomainEvents
  1. Contiene los eventos del dominio del contexto, en este caso es dónde se incluiría el evento UserAccountDeleted:
namespace Mallotore.Users.DomainEvents.DeleteAccount 
{ 
    public class UserAccountDeleted 
    {        
        public string UserId { get; }        
        public string Username { get; }        
        public UserAccountDeleted(string userId, string username){
        	UserId = userId;            
            Username = username;        
        }    
    }
}

 2.  Contiene el contrato de cómo se debe manejar este evento, es decir, la interfaz del handler capaz de gestionarlo IUserAccountDeletedEventHandler:

using LanguageExt;

namespace Mallotore.Users.DomainEvents.DeleteAccount 
{
    public interface IUserAccountDeletedEventHandler 
    {
    	Unit Handle(UserAccountDeleted domainEvent);
    }
}

El comando que emite el evento del domino usará esta interfaz, siendo fácilmente intercambiable por lo que necesitemos. Esto permite que se pueda hacer un handler que ejecute todo síncrono, o uno asíncrono y que publique un mensaje, por ejemplo.

public class DeleteAccount : IDeleteAccount
{
    private readonly UserRepository userRepository;
    private readonly IUserAccountDeletedEventHandler domainEventHandler;

    public DeleteAccount(
    	UserRepository userRepository,
    	IUserAccountDeletedEventHandler domainEventHandler)
    {
    	this.userRepository = userRepository;
    	this.domainEventHandler = domainEventHandler;
    }

    public Either<UserAccountDeletionError, Unit> Execute(UserAccountDeletionRequest request)
    {
        return from user in FindUser(request.UserId)
            from _1 in CheckUserIsNotDeleted(user)
            from _2 in DeleteUserAccount(user, userSubscription)
            from _3 in HandleDomainEvent(request, user)
            select unit;

            Either<UserAccountDeletionError, Unit> HandleDomainEvent(UserAccountDeletionRequest deletionRequest, User user)
            {
                var domainEvent = new UserAccountDeleted(
                    userId: deletionRequest.UserId,
                    username: user.Username);
                return domainEventHandler.Handle(domainEvent);
            }
        
   ...
}
  • Es un proyecto sin dependencias externas (no depende de otros proyectos).
  • Es un proyecto que es candidato a ser paquetizado, es decir, convertido a nuget para ser fácilmente usado desde otros contextos que les interese este evento.
DomainEventsHandlers
  • Es el proyecto de los manejadores de eventos. Los eventos del dominio pueden ser de su propio contexto o de otros si fuera necesario.
  • Es un proyecto con tantas dependencias a proyectos de DomainEvents como sean necesarios.
  1. Implementa la interfaz IUserAccountDeletedEventHandler del proyecto DomainEvents para sus propios intereses. En nuestro ejemplo es donde estará el handler capaz de enviar el email de confirmación al usuario:
using System;
using System.Collections.Generic;
using LanguageExt;
using Mallotore.Common.Logging.Loggers;
using Mallotore.Emails.UserAccountDeletion;
using Mallotore.Users.DomainEvents.DeleteAccount;
using static LanguageExt.Prelude;

namespace Mallotore.Users.DomainEventsHandlers
{
    public class UserAccountDeletedEventHandler : IUserAccountDeletedEventHandler
    {
        private readonly ILogger logger;
        private readonly IUserAccountDeletionNotifier notifier;

        public UserAccountDeletedEventHandler(
            ILogger logger,
            IUserAccountDeletionNotifier notifier)
        {
            this.logger = logger;
            this.notifier = notifier;
        }

        public Unit Handle(UserAccountDeleted domainEvent)
        {
            TryToNotifyToCustomer();
            return unit;        

            void TryToNotifyToCustomer()
            {
                try
                {
                    var request = new UserAccountDeletionRequest(
                        emails: new List<string> { domainEvent.UserId }.AsReadOnly(),
                        username: domainEvent.Username);
                    notifier.Notify(request);
                }
                catch (Exception ex)
                {
                    logger.LogError(ex, $"NotifyToCustomer|userId[{domainEvent.UserId}]");
                }
            }
        }
    }
}
DomainEventsMessageBusHandlers
  • Es el proyecto de la infraestructura con la que hacemos asíncronos nuestros eventos del dominio.
  • Es el proyecto que sabe publicar los eventos del dominio, sabe publicar mensajes.
namespace Mallotore.Users.DomainEventsMessageBusHandlers.DeleteAccount
{
    public class UserAccountDeletedMessage
    {
        public string UserId { get; set; }
        public string Username { get; set; }
    }
}
  • Implementa la interfaz IUserAccountDeletedEventHandler del proyecto DomainEvents:
using LanguageExt;
using Mallotore.Common.Messaging.MessageBus;
using Mallotore.Users.DomainEvents.DeleteAccount;

namespace Mallotore.Users.DomainEventsMessageBusHandlers.DeleteAccount
{
    public class UserAccountDeletedEventPublisher : IUserAccountDeletedEventHandler
    {
        private readonly MessageBus messageBus;

        public UserAccountDeletedEventPublisher(MessageBus messageBus)
        {
            this.messageBus = messageBus;
        }

        public Unit Handle(UserAccountDeleted domainEvent)
        {
            var message = DomainEventMessageConverter.Convert(domainEvent);
            messageBus.Publish(message);
            return Prelude.unit;
        }
    }
}
using Mallotore.Users.DomainEvents.DeleteAccount;

namespace Mallotore.Users.DomainEventsMessageBusHandlers.DeleteAccount
{
    internal static class DomainEventMessageConverter
    {
        public static UserAccountDeletedMessage Convert(UserAccountDeleted domainEvent)
        {
            return new UserAccountDeletedMessage
            {
                UserId = domainEvent.UserId,
                Username = domainEvent.Username
            };
        }
    }
}
  • Es el proyecto que sabe consumir los eventos asíncronos del dominio, sabe consumir mensajes:
    • Son las clases que usarán los susbcribers para inyectar el handler oportuno según necesidad.
using Mallotore.Users.DomainEvents.DeleteAccount;

namespace Mallotore.Users.DomainEventsMessageBusHandlers.DeleteAccount
{
    public interface IUserAccountDeletedMessageConsumer
    {
        void Handle(UserAccountDeletedMessage message);
    }

    public class UserAccountDeletedMessageConsumer : IUserAccountDeletedMessageConsumer
    {
        private readonly IUserAccountDeletedEventHandler handler;

        public UserAccountDeletedMessageConsumer(IUserAccountDeletedEventHandler handler)
        {
            this.handler = handler;
        }

        public void Handle(UserAccountDeletedMessage message)
        {
            var domainEvent = DomainEventConverter.Convert(message);
            handler.Handle(domainEvent);
        }
    }
}
using Mallotore.Users.DomainEvents.DeleteAccount;

namespace Mallotore.Users.DomainEventsMessageBusHandlers.DeleteAccount
{
    internal static class DomainEventConverter
    {
        public static UserAccountDeleted Convert(UserAccountDeletedMessage message)
        {
            return new UserAccountDeleted(
                userId: message.UserId,
                username: message.Username);
        }
    }
}
  • Tiene dependencia con la infraestructura que usamos para publicar (MessageBus).
  • Tiene dependencia con los eventos del dominio de su contexto.
  • Habrá tantos proyectos de este tipo como contextos que tengan gestión de eventos del dominio asíncronos (por bus de mensajes).
  • Es un proyecto que es candidato a ser paquetizado, es decir, convertido a nuget para ser fácilmente usado desde otros contextos que les interese este evento.
  • Internamente la estructura de clases que debería tener por cada evento del dominio sería, considerando que existe la carpeta para nuestro ejemplo DeleteAccount:
    • DomainEventConverter.cs
    • DomainEventMessageConverter.cs
    • UserAccountDeletedEventPublisher.cs
    • UserAccountDeletedMessage.cs
    • UserAccountDeletedMessageConsumer.cs

Contexto de atención al cliente

Proyectos resultantes dentro del contexto de atención al cliente, que es a quién le interesa cuándo un usuario se le da de baja.

DomainEventsHandlers
  • Es el proyecto de los manejadores de eventos.
  • Los eventos del dominio pueden ser de su propio contexto o de otros si fuera necesario.
  • Es un proyecto con tantas dependencias a proyectos de DomainEvents como sean necesarios.
  • Implementa la interfaz IUserAccountDeletedEventHandler del proyecto DomainEvents para sus propios intereses.
  • En nuestro ejemplo es donde estará el handler que actualiza el contacto cuando se da de baja a un usuario.
using LanguageExt;
using Mallotore.Common.Logging.Loggers;
using Mallotore.CustomerSupport.Communications;
using Mallotore.Users.DomainEvents.DeleteAccount;

namespace Mallotore.CustomerSupport.DomainEventsHandlers.Users
{
    public class UserAccountDeletedEventHandler : IUserAccountDeletedEventHandler
    {
        private readonly IRevokeMarketingCommunicationsUserPermission command;
        private readonly ILogger logger;

        public UserAccountDeletedEventHandler(
            IRevokeMarketingCommunicationsUserPermission command,
            ILogger logger)
        {
            this.command = command;
            this.logger = logger;
        }

        public Unit Handle(UserAccountDeleted domainEvent)
        {
            var request = MarketingCommunicationsUserPermissionRevocationRequest.CreateFromBusiness(
                userId: domainEvent.UserId);
            command.Execute(request).MapLeft(LogError);
            return Prelude.unit;

            Unit LogError(Errors error)
            {
              	logger.LogInfo($"UserAccountDeleted[{domainEvent.UserId}] error[{error}]");
                return Prelude.unit;
            }
        }
    }
}

Subscribers

  • En el patrón Publish/Subscribe sería nuestro proyecto de infraestructura que se comunicaría con el bus de eventos, en nuestro caso RabbitMq.
  • En un mundo ideal habría tantos Subscribers como contextos diferentes, siendo este subscriber la manera de consumir eventos vía mensajería de otros contextos pero relevantes para el nuestro.
  • Usa el proyecto DomainEventsMessageBusHandlers para consumir el mensaje y convertirlo a evento del dominio, inyectando el handler correspondiente por contexto.
//SubscriberProcess users
....
private readonly IUserAccountDeletedMessageConsumer userAccountDeletedMessageConsumer;
....
           messageBus.SubscribeAsync<UserAccountDeletedMessage>(
           		subscriptionId: "users",
                onMessage: message => Task.Factory.StartNew(() =>
                        {                         userAccountDeletedMessageConsumer.Handle(message);
                        }
                ));
....
//SubscriberProcess customer support
....
private readonly IUserAccountDeletedMessageConsumer userAccountDeletedMessageConsumer;
....
           messageBus.SubscribeAsync<UserAccountDeletedMessage>(
           		subscriptionId: "customer_support",
                onMessage: message => Task.Factory.StartNew(() =>
                        {                         userAccountDeletedMessageConsumer.Handle(message);
                        }
                ));
....
Resumen - Vista general

Share Tweet Send
0 Comentarios
Cargando...