A source generator library that automatically creates domain event records from domain method signatures.
dotnet add package Mesch.DomainEventsMark domain classes with [GenerateDomainEvents] and methods with [DomainEvent]. The source generator creates event records and helper classes at compile time.
using Mesch.DomainEvents;
[GenerateDomainEvents(EventNamespace = "MyApp.Events")]
public partial record Customer
{
[DomainEvent(EventType = DomainEventType.Created)]
public static Result<Customer> Create(string name, string email) { }
}The source generator produces:
- Event record implementing
IAggregateEvent - Static helper class with factory methods
- All events placed in the specified namespace
Class-level configuration:
EventNamespace- Target namespace for generated eventsEventSuffix- Suffix appended to event names (default: "Event")GenerateHelperClass- Whether to generate factory methods (default: true)HelperClassName- Name of the helper class (default: "{ClassName}Events")
Method-level configuration:
EventName- Custom event name (default: derived from method name)EventType- Type of domain operation (Created, Updated, Deleted, Command, Custom)IncludeParameters- Specific parameters to includeExcludeParameters- Parameters to exclude from eventIncludeAggregateProperties- Current aggregate properties to includeIncludeResult- Whether to include method return value
Events are stored using a custom Result type that provides a clean, type-safe approach to handling success and error cases.
Events can be attached to results using several methods:
// Using generated helper
var eventData = CustomerEvents.CreateCreateEvent(id, name, email);
return ResultEventsExtensions.Ok(customer).WithEvents(eventData);
// Multiple events
return result.WithEvents(event1, event2, event3);
// Create result with events directly
return ResultEventsExtensions.OkWithEvents(customer, eventData);Events can be retrieved from results for persistence or processing:
var result = Customer.Create("John", "john@example.com");
// Get all events
var allEvents = result.GetEvents();
// Get specific event types
var domainEvents = result.GetDomainEvents();
var createEvents = result.GetEventsOfType<CustomerCreateEvent>();
// Check for events
bool hasEvents = result.HasEvents();
bool hasCreateEvents = result.HasEventsOfType<CustomerCreateEvent>();A typical persistence pattern:
public async Task<Result<Customer>> CreateCustomerAsync(string name, string email)
{
var result = Customer.Create(name, email);
if (result.IsSuccess)
{
// Persist aggregate
await repository.SaveAsync(result.Value);
// Persist events
var events = result.GetDomainEvents();
await eventStore.SaveEventsAsync(events);
// Or publish events
foreach (var evt in events)
{
await eventBus.PublishAsync(evt);
}
}
return result;
}Generated events implement IAggregateEvent with standard properties:
Timestamp- Event creation time (UTC)CorrelationId- Unique identifier for event correlationEventType- String identifier for the event typeVersion- Schema version for compatibilityAggregateId- ID of the related aggregateAggregateType- Type name of the aggregate
After calling Result.Ok(tenant).WithEvents(eventData), callers can access events using a variety of extension methods.
Returns all events as List<object>:
var result = Tenant.Create("Acme", "acme", "admin@acme.com");
var allEvents = result.GetEvents(); // List<object>Returns events that implement IDomainEvent:
var result = Tenant.Create("Acme", "acme", "admin@acme.com");
var domainEvents = result.GetDomainEvents(); // IEnumerable<IDomainEvent>
foreach (var evt in domainEvents)
{
Console.WriteLine($"{evt.EventType} at {evt.Timestamp}");
Console.WriteLine($"Correlation: {evt.CorrelationId}");
}Returns events of a specific type:
var result = Tenant.Create("Acme", "acme", "admin@acme.com");
// Get specific event types
var createEvents = result.GetEventsOfType<TenantCreateEvent>();
var updateEvents = result.GetEventsOfType<TenantUpdateNameEvent>();
foreach (var createEvent in createEvents)
{
Console.WriteLine($"Created: {createEvent.Name} ({createEvent.Subdomain})");
Console.WriteLine($"Admin: {createEvent.AdministratorEmailAddress}");
}Check if any events exist:
var result = Tenant.Create("Acme", "acme", "admin@acme.com");
if (result.HasEvents())
{
Console.WriteLine("Operation generated events");
await ProcessEventsAsync(result.GetDomainEvents());
}Check for specific event types:
var result = Tenant.Create("Acme", "acme", "admin@acme.com");
if (result.HasEventsOfType<TenantCreateEvent>())
{
var events = result.GetEventsOfType<TenantCreateEvent>();
await HandleCreationEventsAsync(events);
}var result = Tenant.Create("Acme", "acme", "admin@acme.com");
if (result.IsSuccess && result.HasEvents())
{
var events = result.GetDomainEvents();
await _eventPublisher.PublishAsync(events);
}var result = Tenant.Create("Acme", "acme", "admin@acme.com");
if (result.IsSuccess)
{
// Handle creation events
var createEvents = result.GetEventsOfType<TenantCreateEvent>();
foreach (var evt in createEvents)
{
await _notificationService.SendWelcomeEmailAsync(evt.AdministratorEmailAddress);
await _auditService.LogTenantCreationAsync(evt);
}
}var result = Tenant.Create("Acme", "acme", "admin@acme.com");
// Pattern matching on the result itself
await result.Match(
async tenant =>
{
foreach (var evt in result.GetDomainEvents())
{
switch (evt)
{
case TenantCreateEvent createEvent:
await HandleTenantCreated(createEvent);
break;
case TenantUpdateNameEvent updateEvent:
await HandleTenantNameUpdated(updateEvent);
break;
default:
await HandleGenericEvent(evt);
break;
}
}
},
async error => await LogError(error)
);var allEvents = new List<IDomainEvent>();
var result1 = Tenant.Create("Acme", "acme", "admin@acme.com");
var result2 = result1.IsSuccess
? result1.Value.UpdateName("Acme Corp", PersonId.New())
: ResultEventsExtensions.Fail<Tenant>("Failed");
if (result1.IsSuccess) allEvents.AddRange(result1.GetDomainEvents());
if (result2.IsSuccess) allEvents.AddRange(result2.GetDomainEvents());
// Process all events together
await _eventStore.SaveEventsAsync(allEvents);[HttpPost]
public async Task<IActionResult> CreateTenant(CreateTenantRequest request)
{
var result = Tenant.Create(request.Name, request.Subdomain, request.AdminEmail);
return result.Match<IActionResult>(
tenant =>
{
// Extract events for background processing
var events = result.GetDomainEvents();
_backgroundJobService.EnqueueEventsAsync(events);
return Ok(new
{
TenantId = tenant.Id,
EventCount = events.Count()
});
},
error => BadRequest(new { error.Message })
);
}All generated events implement IAggregateEvent and provide:
DateTime Timestamp- When the event occurredstring CorrelationId- Unique correlation identifierstring EventType- Type of event (e.g., "TenantCreateEvent")int Version- Schema version for compatibility
string AggregateId- ID of the aggregate that generated the eventstring AggregateType- Type name of the aggregate (e.g., "Tenant")
- Domain-specific properties based on method parameters
- Aggregate properties (if configured with
IncludeAggregateProperties)
public async Task SaveTenantAsync(Tenant tenant, IEnumerable<IDomainEvent> events)
{
await _tenantRepository.SaveAsync(tenant);
await _eventStore.AppendEventsAsync(tenant.Id.ToString(), events);
}public async Task PublishEventsAsync(IEnumerable<IDomainEvent> events)
{
foreach (var evt in events)
{
await _messageBus.PublishAsync(evt.EventType, evt);
}
}public async Task UpdateReadModelsAsync(IEnumerable<IDomainEvent> events)
{
foreach (var evt in events.OfType<TenantCreateEvent>())
{
await _tenantReadModelService.CreateAsync(new TenantReadModel
{
Id = evt.AggregateId,
Name = evt.Name,
Subdomain = evt.Subdomain,
CreatedAt = evt.Timestamp
});
}
}This API provides a clean, strongly-typed way to access and process domain events generated by your domain methods.
The library consists of three components:
Mesch.DomainEvents- Main library with extensions and interfacesMesch.DomainEvents.Abstractions- Attributes and interfaces for source generatorMesch.DomainEvents.SourceGenerator- Compile-time code generation (packaged as analyzer)
All components are included in the single Mesch.DomainEvents package.
- .NET 8.0 or later
- C# 11.0 or later
The library uses a custom Result<T> type that provides:
- Compile-time enforcement of error handling
- Pattern matching support via
Match<TResult>()method - Implicit conversions from values and errors
Result<T>.Success(value)- Create successful resultResult<T>.Failure(error)- Create error resultresult.IsSuccess- Check if successful (property)result.IsError- Check if error (property)result.Value- Get value or throw (property)result.Error- Get error or throw (property)result.TryGetValue(out value)- Try to get value safelyresult.TryGetError(out error)- Try to get error safelyresult.Match(onSuccess, onError)- Pattern match (2 overloads: returning TResult and void)
- Zero Dependencies: No external packages required
- Type Safety: Strong typing ensures compile-time safety
- Functional Approach: Pattern matching and immutability
- Performance: Lightweight implementation with minimal allocations
- Clean API: Intuitive and easy to use