Events - PogovorovDaniil/Requestum GitHub Wiki
Events
Events are notifications about important things that have happened in your system. This guide covers everything you need to know about working with events in Requestum.
📋 What is an Event?
An event is a notification about something that has already happened. Events:
- Represent facts that occurred in the past
- Do not modify state directly (but receivers might)
- Do not return values
- Can have zero or more receivers
- Use past tense naming
🎯 Events vs Commands
| Aspect | Command | Event |
|---|---|---|
| Intent | Tell system to do something | Notify that something happened |
| Naming | Imperative (CreateUser) | Past tense (UserCreated) |
| Handlers | Exactly one | Zero or more receivers |
| Timing | Before action | After action |
| Certainty | May fail | Already happened |
📝 Creating Events
Basic Event
public record UserCreatedEvent : IEventMessage
{
public required int UserId { get; init; }
public required string Name { get; init; }
public required string Email { get; init; }
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
}
Event with Constructor
public record UserCreatedEvent(
int UserId,
string Name,
string Email,
DateTime CreatedAt) : IEventMessage;
Simple Event
public record CacheRefreshedEvent : IEventMessage;
public record OrderCancelledEvent(int OrderId) : IEventMessage;
Complex Event
public record OrderPlacedEvent : IEventMessage
{
public required int OrderId { get; init; }
public required int CustomerId { get; init; }
public required decimal TotalAmount { get; init; }
public required List<OrderItemDto> Items { get; init; }
public required DateTime PlacedAt { get; init; }
public string? DiscountCode { get; init; }
}
public record OrderItemDto(int ProductId, string ProductName, int Quantity, decimal Price);
🔧 Creating Event Receivers
Synchronous Receiver
Use for simple, non-I/O operations:
public class LogUserCreationReceiver : IEventMessageReceiver<UserCreatedEvent>
{
private readonly ILogger<LogUserCreationReceiver> _logger;
public LogUserCreationReceiver(ILogger<LogUserCreationReceiver> logger)
{
_logger = logger;
}
public void Receive(UserCreatedEvent message)
{
_logger.LogInformation(
"User created: {UserId} - {Name} ({Email})",
message.UserId,
message.Name,
message.Email);
}
}
Asynchronous Receiver
Use for I/O operations:
public class SendWelcomeEmailReceiver : IAsyncEventMessageReceiver<UserCreatedEvent>
{
private readonly IEmailService _emailService;
private readonly ILogger<SendWelcomeEmailReceiver> _logger;
public SendWelcomeEmailReceiver(
IEmailService emailService,
ILogger<SendWelcomeEmailReceiver> logger)
{
_emailService = emailService;
_logger = logger;
}
public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken ct = default)
{
_logger.LogInformation("Sending welcome email to {Email}", message.Email);
await _emailService.SendWelcomeEmailAsync(
message.Email,
message.Name,
ct);
_logger.LogInformation("Welcome email sent to {Email}", message.Email);
}
}
Multiple Receivers for Same Event
One event can have multiple receivers:
// Receiver 1: Send email
public class SendWelcomeEmailReceiver : IAsyncEventMessageReceiver<UserCreatedEvent>
{
public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken ct = default)
{
await _emailService.SendWelcomeEmailAsync(message.Email, message.Name, ct);
}
}
// Receiver 2: Update analytics
public class TrackUserRegistrationReceiver : IAsyncEventMessageReceiver<UserCreatedEvent>
{
public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken ct = default)
{
await _analyticsService.TrackUserRegistrationAsync(message.UserId, ct);
}
}
// Receiver 3: Log to file
public class LogUserCreationToFileReceiver : IAsyncEventMessageReceiver<UserCreatedEvent>
{
public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken ct = default)
{
await _fileLogger.LogAsync($"User {message.UserId} created at {message.CreatedAt}", ct);
}
}
// Receiver 4: Notify admin
public class NotifyAdminOfNewUserReceiver : IAsyncEventMessageReceiver<UserCreatedEvent>
{
public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken ct = default)
{
await _notificationService.NotifyAdminAsync($"New user: {message.Name}", ct);
}
}
🚀 Publishing Events
Publish with Instance
var eventMessage = new UserCreatedEvent(
UserId: 123,
Name: "Alice",
Email: "[email protected]",
CreatedAt: DateTime.UtcNow
);
// Synchronous
requestum.Publish(eventMessage);
// Asynchronous
await requestum.PublishAsync(eventMessage);
// With cancellation token
await requestum.PublishAsync(eventMessage, cancellationToken);
Publish without Instance
For events with parameterless constructors:
// Synchronous
requestum.Publish<CacheRefreshedEvent>();
// Asynchronous
await requestum.PublishAsync<CacheRefreshedEvent>();
Publishing from Command Handler
public class CreateUserCommandHandler : IAsyncCommandHandler<CreateUserCommand>
{
private readonly IUserRepository _repository;
private readonly IRequestum _requestum;
public CreateUserCommandHandler(IUserRepository repository, IRequestum requestum)
{
_repository = repository;
_requestum = requestum;
}
public async Task ExecuteAsync(CreateUserCommand command, CancellationToken ct = default)
{
// Execute command
var user = new User(command.Name, command.Email);
await _repository.AddAsync(user, ct);
// Publish event
await _requestum.PublishAsync(
new UserCreatedEvent(user.Id, user.Name, user.Email, DateTime.UtcNow),
ct);
}
}
🎨 Event Patterns
Domain Events
Events that represent domain-specific business events:
public record OrderPlacedEvent(
int OrderId,
int CustomerId,
decimal TotalAmount,
DateTime PlacedAt) : IEventMessage;
public record OrderShippedEvent(
int OrderId,
string TrackingNumber,
DateTime ShippedAt) : IEventMessage;
public record OrderDeliveredEvent(
int OrderId,
DateTime DeliveredAt) : IEventMessage;
Integration Events
Events for cross-system communication:
public record PaymentProcessedEvent : IEventMessage
{
public required string ExternalPaymentId { get; init; }
public required int OrderId { get; init; }
public required decimal Amount { get; init; }
public required string Currency { get; init; }
public required DateTime ProcessedAt { get; init; }
}
// Receiver that publishes to message queue
public class PublishPaymentToQueueReceiver : IAsyncEventMessageReceiver<PaymentProcessedEvent>
{
private readonly IMessageQueue _queue;
public async Task ReceiveAsync(PaymentProcessedEvent message, CancellationToken ct = default)
{
await _queue.PublishAsync("payments", message, ct);
}
}
Event Sourcing
Using events to build system state:
public record AccountCreatedEvent(string AccountId, decimal InitialBalance) : IEventMessage;
public record MoneyDepositedEvent(string AccountId, decimal Amount) : IEventMessage;
public record MoneyWithdrawnEvent(string AccountId, decimal Amount) : IEventMessage;
public class AccountBalanceProjection :
IAsyncEventMessageReceiver<AccountCreatedEvent>,
IAsyncEventMessageReceiver<MoneyDepositedEvent>,
IAsyncEventMessageReceiver<MoneyWithdrawnEvent>
{
private readonly IAccountRepository _repository;
public async Task ReceiveAsync(AccountCreatedEvent message, CancellationToken ct = default)
{
await _repository.CreateAccountAsync(message.AccountId, message.InitialBalance, ct);
}
public async Task ReceiveAsync(MoneyDepositedEvent message, CancellationToken ct = default)
{
await _repository.UpdateBalanceAsync(message.AccountId, balance => balance + message.Amount, ct);
}
public async Task ReceiveAsync(MoneyWithdrawnEvent message, CancellationToken ct = default)
{
await _repository.UpdateBalanceAsync(message.AccountId, balance => balance - message.Amount, ct);
}
}
Cascading Events
One event triggering another:
// First event
public record OrderPlacedEvent(int OrderId, int CustomerId, decimal Total) : IEventMessage;
// Receiver that publishes another event
public class ProcessOrderPaymentReceiver : IAsyncEventMessageReceiver<OrderPlacedEvent>
{
private readonly IPaymentService _paymentService;
private readonly IRequestum _requestum;
public async Task ReceiveAsync(OrderPlacedEvent message, CancellationToken ct = default)
{
var paymentId = await _paymentService.ProcessPaymentAsync(
message.CustomerId,
message.Total,
ct);
// Publish another event
await _requestum.PublishAsync(
new PaymentProcessedEvent(paymentId, message.OrderId, message.Total),
ct);
}
}
⚙️ Configuration
Requiring Event Handlers
By default, Requestum requires at least one receiver for each event:
services.AddRequestum(cfg =>
{
cfg.RequireEventHandlers = true; // Default
cfg.RegisterHandlers(typeof(Program).Assembly);
});
// Publishing event without receivers will throw exception
await requestum.PublishAsync(new UserCreatedEvent(...)); // ❌ Throws if no receivers
Allowing Events without Handlers
services.AddRequestum(cfg =>
{
cfg.RequireEventHandlers = false; // Allow events without receivers
cfg.RegisterHandlers(typeof(Program).Assembly);
});
// Publishing event without receivers is allowed (no-op)
await requestum.PublishAsync(new UserCreatedEvent(...)); // ✅ No error, silently ignored
🛡️ Error Handling
Exception in One Receiver
If one receiver throws an exception, it doesn't prevent other receivers from executing:
// Receiver 1: Throws exception
public class SendEmailReceiver : IAsyncEventMessageReceiver<UserCreatedEvent>
{
public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken ct = default)
{
throw new Exception("Email service is down"); // This throws
}
}
// Receiver 2: Will still execute
public class LogUserReceiver : IAsyncEventMessageReceiver<UserCreatedEvent>
{
public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken ct = default)
{
_logger.LogInformation("User created: {UserId}", message.UserId); // Still runs
}
}
Handling Exceptions in Receivers
public class SendWelcomeEmailReceiver : IAsyncEventMessageReceiver<UserCreatedEvent>
{
private readonly IEmailService _emailService;
private readonly ILogger<SendWelcomeEmailReceiver> _logger;
public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken ct = default)
{
try
{
await _emailService.SendWelcomeEmailAsync(message.Email, message.Name, ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send welcome email to {Email}", message.Email);
// Don't rethrow - allow other receivers to execute
}
}
}
✅ Best Practices
1. Use Past Tense Naming
// ✅ Good - past tense
public record UserCreatedEvent(...) : IEventMessage;
public record OrderPlacedEvent(...) : IEventMessage;
public record PaymentProcessedEvent(...) : IEventMessage;
// ❌ Bad - present or future tense
public record CreateUserEvent(...) : IEventMessage;
public record PlaceOrderEvent(...) : IEventMessage;
public record ProcessPaymentEvent(...) : IEventMessage;
2. Include Relevant Data
// ✅ Good - includes all relevant data
public record UserCreatedEvent(
int UserId,
string Name,
string Email,
DateTime CreatedAt) : IEventMessage;
// ❌ Bad - missing important data
public record UserCreatedEvent(int UserId) : IEventMessage;
3. Make Events Immutable
// ✅ Good - immutable
public record UserCreatedEvent(int UserId, string Name, string Email) : IEventMessage;
// ❌ Bad - mutable
public class UserCreatedEvent : IEventMessage
{
public int UserId { get; set; }
public string Name { get; set; }
}
4. Keep Receivers Independent
// ✅ Good - independent receivers
public class SendEmailReceiver : IAsyncEventMessageReceiver<UserCreatedEvent>
{
public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken ct = default)
{
// Only sends email, doesn't care about other receivers
await _emailService.SendWelcomeEmailAsync(message.Email, message.Name, ct);
}
}
// ❌ Bad - coupled receivers
public class SendEmailReceiver : IAsyncEventMessageReceiver<UserCreatedEvent>
{
public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken ct = default)
{
await _emailService.SendWelcomeEmailAsync(message.Email, message.Name, ct);
// Don't call other receivers directly
await new LogUserReceiver().ReceiveAsync(message, ct); // ❌ Bad
}
}
5. Don't Perform Heavy Operations
// ✅ Good - queue heavy work
public class ProcessOrderReceiver : IAsyncEventMessageReceiver<OrderPlacedEvent>
{
public async Task ReceiveAsync(OrderPlacedEvent message, CancellationToken ct = default)
{
// Queue for background processing
await _backgroundJobQueue.EnqueueAsync(
new ProcessOrderJob(message.OrderId),
ct);
}
}
// ❌ Bad - heavy synchronous work
public class ProcessOrderReceiver : IAsyncEventMessageReceiver<OrderPlacedEvent>
{
public async Task ReceiveAsync(OrderPlacedEvent message, CancellationToken ct = default)
{
// This blocks for a long time
await Task.Delay(TimeSpan.FromMinutes(10), ct); // ❌ Bad
}
}
6. Include Timestamps
// ✅ Good - includes when event occurred
public record UserCreatedEvent(
int UserId,
string Name,
string Email,
DateTime CreatedAt) : IEventMessage;
// Also good - use DateTimeOffset for timezone awareness
public record UserCreatedEvent(
int UserId,
string Name,
string Email,
DateTimeOffset OccurredAt) : IEventMessage;
🎓 Next Steps
- Middleware Pipeline - Add cross-cutting concerns to events