Skip to main content

Event Handling

Events enable loosely coupled communication between different parts of your application and with external systems. This architecture supports both domain events (internal) and integration events (external).

Event Types

Domain Events

Domain events represent something that happened within your domain that other parts of the application care about. They:
  • Are published and consumed within the same application
  • Represent domain-specific occurrences
  • Are handled synchronously
  • Inherit from DomainEvent

Integration Events

Integration events communicate with external systems or microservices. They:
  • Are published to message brokers (RabbitMQ, etc.)
  • Cross application boundaries
  • Are handled asynchronously
  • Inherit from IntegrationEvent

Domain Events

1

Create a Domain Event

Create a domain event class that inherits from DomainEvent:
using Core.Application;
using static Domain.Enums.Enums;

namespace Application.DomainEvents
{
    /// <summary>
    /// Domain event raised when a YourEntity is created
    /// </summary>
    internal sealed class YourEntityCreated : DomainEvent
    {
        public string Id { get; set; }
        public string PropertyOne { get; set; }
        public int PropertyTwo { get; set; }
        public DateTime CreatedAt { get; set; }
    }
}
Example from codebase: Application/DomainEvents/DummyEntityCreated.cs:10-16
2

Publish Domain Event from Use Case

Publish the domain event after a successful operation:
using Application.DomainEvents;
using Core.Application;

namespace Application.UseCases.YourEntity.Commands.CreateYourEntity
{
    internal sealed class CreateYourEntityHandler : IRequestCommandHandler<CreateYourEntityCommand, string>
    {
        private readonly ICommandQueryBus _domainBus;
        private readonly IYourEntityRepository _repository;

        public CreateYourEntityHandler(
            ICommandQueryBus domainBus,
            IYourEntityRepository repository)
        {
            _domainBus = domainBus;
            _repository = repository;
        }

        public async Task<string> Handle(CreateYourEntityCommand request, CancellationToken cancellationToken)
        {
            // 1. Create entity
            var entity = new Domain.Entities.YourEntity(
                request.PropertyOne,
                request.PropertyTwo
            );

            // 2. Validate
            if (!entity.IsValid)
                throw new InvalidEntityDataException(entity.GetErrors());

            // 3. Persist
            object createdId = await _repository.AddAsync(entity);

            // 4. Publish domain event
            await _domainBus.Publish(entity.To<YourEntityCreated>(), cancellationToken);

            return createdId.ToString();
        }
    }
}
Key method: entity.To<YourEntityCreated>() maps the entity to the eventReference: Application/UseCases/DummyEntity/Commands/CreateDummyEntity/CreateDummyEntityHandler.cs:34
3

Create Domain Event Handler

Create a handler to respond to the domain event:
using Application.DomainEvents;
using Core.Application;

namespace Application.DomainEvents.Handlers
{
    internal sealed class YourEntityCreatedHandler : IDomainEventHandler<YourEntityCreated>
    {
        private readonly ILogger<YourEntityCreatedHandler> _logger;
        private readonly IEmailService _emailService;

        public YourEntityCreatedHandler(
            ILogger<YourEntityCreatedHandler> logger,
            IEmailService emailService)
        {
            _logger = logger;
            _emailService = emailService;
        }

        public async Task Handle(YourEntityCreated @event, CancellationToken cancellationToken)
        {
            // Log the event
            _logger.LogInformation($"YourEntity created: {@event.Id}");

            // Send notification email
            await _emailService.SendAsync(
                to: "admin@example.com",
                subject: "New Entity Created",
                body: $"Entity {@event.Id} was created with value {@event.PropertyOne}"
            );

            // Other side effects...
        }
    }
}

Integration Events

1

Create Integration Event

Create an integration event class that inherits from IntegrationEvent:
using Core.Application;

namespace Application.Integrations.Events
{
    public class YourEntityCreatedIntegrationEvent : IntegrationEvent
    {
        public YourEntityCreatedIntegrationEvent()
        {
        }

        public YourEntityCreatedIntegrationEvent(
            string eventType,
            string subject,
            object data) : base(eventType, subject, data)
        {
        }

        public YourEntityCreatedIntegrationEvent(
            Guid id,
            DateTime date,
            string eventType,
            string subject,
            object data) : base(id, date, eventType, subject, data)
        {
        }
    }
}
Example from codebase: Application/Integrations/Events/DummyEntityCreatedIntegrationEvent.cs:5-20
2

Create Integration Event Publisher

Create a publisher handler that converts domain events to integration events:
using Application.DomainEvents;
using Application.Integrations.Events;
using Core.Application;

namespace Application.Integrations.Handlers.Publishers
{
    public class YourEntityCreatedIntegrationEventHandlerPub : IIntegrationEventHandler<YourEntityCreatedIntegrationEvent>
    {
        private readonly IEventBus _eventBus;

        public YourEntityCreatedIntegrationEventHandlerPub(IEventBus eventBus)
        {
            _eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus));
        }

        public Task Handle(YourEntityCreatedIntegrationEvent @event)
        {
            // Publish to external message broker (RabbitMQ, etc.)
            _eventBus.Publish(@event);

            return Task.CompletedTask;
        }
    }
}
Reference: Application/Integrations/Handlers/Publishers/DummyEntityCreatedIntegrationEventHandlerPub.cs:5-21
3

Create Integration Event Subscriber

Create a subscriber handler to consume integration events from external systems:
using Application.Integrations.Events;
using Core.Application;

namespace Application.Integrations.Handlers.Subscribers
{
    public class YourEntityCreatedIntegrationEventHandlerSub : IIntegrationEventHandler<YourEntityCreatedIntegrationEvent>
    {
        private readonly ILogger<YourEntityCreatedIntegrationEventHandlerSub> _logger;
        private readonly ICommandQueryBus _commandQueryBus;

        public YourEntityCreatedIntegrationEventHandlerSub(
            ILogger<YourEntityCreatedIntegrationEventHandlerSub> logger,
            ICommandQueryBus commandQueryBus)
        {
            _logger = logger;
            _commandQueryBus = commandQueryBus;
        }

        public async Task Handle(YourEntityCreatedIntegrationEvent @event)
        {
            _logger.LogInformation($"Received integration event: {@event.Id}");

            // Process the event
            // For example, sync data to a read model
            var data = @event.Data as YourEntityData;
            
            // Execute command or query based on the event
            // await _commandQueryBus.Send(new SyncDataCommand { ... });

            return Task.CompletedTask;
        }
    }
}
Reference: Application/Integrations/Handlers/Subscribers/DummyEntityCreatedIntegrationEventHandlerSub.cs:5-12
4

Register Event Subscriptions

Register event subscriptions in Startup.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ... other middleware

    UseEventBus(app);
    
    // ... other configuration
}

private void UseEventBus(IApplicationBuilder app)
{
    var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
    
    // Subscribe to integration events
    eventBus.Subscribe<YourEntityCreatedIntegrationEvent, YourEntityCreatedIntegrationEventHandlerSub>();
    eventBus.Subscribe<YourEntityUpdatedIntegrationEvent, YourEntityUpdatedIntegrationEventHandlerSub>();
    eventBus.Subscribe<YourEntityDeletedIntegrationEvent, YourEntityDeletedIntegrationEventHandlerSub>();
}
Reference: Template-API/Startup.cs:65-71

RabbitMQ Configuration

Configure Connection String

Add RabbitMQ connection string to appsettings.json:
{
  "RabbitMqEventBus": {
    "Connectionstring": "amqps://username:password@server.cloudamqp.com/vhost"
  }
}
For local RabbitMQ:
{
  "RabbitMqEventBus": {
    "Connectionstring": "amqp://guest:guest@localhost:5672/"
  }
}
Reference: Template-API/appsettings.json:16-18

Register Event Bus

The event bus is registered in the infrastructure services:
public static IServiceCollection AddInfrastructureServices(
    this IServiceCollection services,
    IConfiguration configuration)
{
    // Register RabbitMQ Event Bus
    services.AddSingleton<IEventBus, RabbitMqEventBus>(sp =>
    {
        var connectionString = configuration["RabbitMqEventBus:Connectionstring"];
        var logger = sp.GetRequiredService<ILogger<RabbitMqEventBus>>();
        return new RabbitMqEventBus(connectionString, logger, sp);
    });

    return services;
}

Event Flow Example

Here’s a complete example of how events flow through the system:

1. User Creates Entity

POST /api/v1/YourEntity
Content-Type: application/json

{
  "propertyOne": "test",
  "propertyTwo": 42
}

2. Command Handler Creates Entity and Publishes Domain Event

public async Task<string> Handle(CreateYourEntityCommand request, CancellationToken cancellationToken)
{
    var entity = new YourEntity(request.PropertyOne, request.PropertyTwo);
    object createdId = await _repository.AddAsync(entity);
    
    // Publish domain event
    await _domainBus.Publish(entity.To<YourEntityCreated>(), cancellationToken);
    
    return createdId.ToString();
}

3. Domain Event Handler Processes Event

public async Task Handle(YourEntityCreated @event, CancellationToken cancellationToken)
{
    // Send email notification
    await _emailService.SendAsync("admin@example.com", "New Entity", $"Created: {@event.Id}");
    
    // Publish integration event for external systems
    var integrationEvent = new YourEntityCreatedIntegrationEvent(
        eventType: "YourEntity.Created",
        subject: @event.Id,
        data: @event
    );
    await _integrationEventBus.Publish(integrationEvent);
}

4. Integration Event Published to RabbitMQ

public Task Handle(YourEntityCreatedIntegrationEvent @event)
{
    _eventBus.Publish(@event); // Publishes to RabbitMQ
    return Task.CompletedTask;
}

5. External Services Consume Integration Event

Other microservices subscribed to the event receive and process it:
public async Task Handle(YourEntityCreatedIntegrationEvent @event)
{
    // Sync to read database
    await _readModelRepository.AddAsync(@event.Data);
    
    // Update cache
    await _cache.InvalidateAsync($"entities:{@event.Subject}");
    
    // Trigger other workflows
    await _workflowEngine.StartAsync("OnEntityCreated", @event.Data);
}

Best Practices

Use domain events when:
  • Multiple parts of your application need to react to domain changes
  • You want to decouple handlers from use cases
  • Side effects should happen after the main operation succeeds
  • You need to maintain consistency within a single transaction
Examples:
  • Send email after user registration
  • Update statistics after entity creation
  • Invalidate cache after data modification
Use integration events when:
  • Communicating with external systems or microservices
  • Changes need to be propagated across service boundaries
  • You need asynchronous, distributed processing
  • Implementing event sourcing or CQRS read models
Examples:
  • Notify other microservices of data changes
  • Sync data to reporting databases
  • Trigger workflows in external systems
  • Update search indexes
Follow these naming patterns:Domain Events:
  • Past tense: YourEntityCreated, OrderPlaced, PaymentProcessed
  • Specific to domain: ProductAddedToCart, ShipmentDispatched
Integration Events:
  • Include context: YourEntityCreatedIntegrationEvent
  • Use namespaced event types: "Catalog.Product.Created"
Domain Events:
  • Failures roll back the entire transaction
  • Use try-catch to handle specific scenarios
  • Log errors for debugging
Integration Events:
  • Implement retry logic for transient failures
  • Use dead letter queues for failed messages
  • Monitor and alert on processing failures
  • Consider idempotency for duplicate messages
public async Task Handle(YourEntityCreatedIntegrationEvent @event)
{
    try
    {
        await ProcessEventAsync(@event);
    }
    catch (TransientException ex)
    {
        // Retry logic
        throw; // RabbitMQ will retry
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to process event {@Event}", @event);
        // Send to dead letter queue
    }
}

Testing Events

Unit Testing Event Publishing

[Fact]
public async Task CreateEntity_ShouldPublishDomainEvent()
{
    // Arrange
    var mockBus = new Mock<ICommandQueryBus>();
    var mockRepo = new Mock<IYourEntityRepository>();
    var handler = new CreateYourEntityHandler(mockBus.Object, mockRepo.Object);
    var command = new CreateYourEntityCommand { PropertyOne = "test", PropertyTwo = 42 };

    // Act
    await handler.Handle(command, CancellationToken.None);

    // Assert
    mockBus.Verify(
        x => x.Publish(It.IsAny<YourEntityCreated>(), It.IsAny<CancellationToken>()),
        Times.Once
    );
}

Integration Testing with RabbitMQ

[Fact]
public async Task IntegrationEvent_ShouldBePublishedToRabbitMQ()
{
    // Arrange
    var eventBus = _serviceProvider.GetRequiredService<IEventBus>();
    var @event = new YourEntityCreatedIntegrationEvent(
        "YourEntity.Created",
        "123",
        new { Id = "123", Name = "Test" }
    );

    // Act
    eventBus.Publish(@event);

    // Assert - verify message in RabbitMQ
    await Task.Delay(1000); // Wait for async processing
    // Add assertions based on your message verification strategy
}

Next Steps

Docker Deployment

Deploy your application with Docker and RabbitMQ

Implementing Use Cases

Learn more about commands and queries

Build docs developers (and LLMs) love