Mesch.DomainEvents 0.0.1

dotnet add package Mesch.DomainEvents --version 0.0.1
                    
NuGet\Install-Package Mesch.DomainEvents -Version 0.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Mesch.DomainEvents" Version="0.0.1">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Mesch.DomainEvents" Version="0.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Mesch.DomainEvents">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Mesch.DomainEvents --version 0.0.1
                    
#r "nuget: Mesch.DomainEvents, 0.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Mesch.DomainEvents@0.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Mesch.DomainEvents&version=0.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Mesch.DomainEvents&version=0.0.1
                    
Install as a Cake Tool

Mesch.DomainEvents

A source generator library that automatically creates domain event records from domain method signatures.

Installation

dotnet add package Mesch.DomainEvents

Usage

Basic Setup

Mark 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) { }
}

Generated Output

The source generator produces:

  • Event record implementing IAggregateEvent
  • Static helper class with factory methods
  • All events placed in the specified namespace

Configuration

Class-level configuration:

  • EventNamespace - Target namespace for generated events
  • EventSuffix - 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 include
  • ExcludeParameters - Parameters to exclude from event
  • IncludeAggregateProperties - Current aggregate properties to include
  • IncludeResult - Whether to include method return value

Event Capture

Events are stored using a custom Result<T> type that provides a clean, type-safe approach to handling success and error cases.

Adding Events

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);

Extracting Events

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>();

Event Persistence

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;
}

Event Schema

Generated events implement IAggregateEvent with standard properties:

  • Timestamp - Event creation time (UTC)
  • CorrelationId - Unique identifier for event correlation
  • EventType - String identifier for the event type
  • Version - Schema version for compatibility
  • AggregateId - ID of the related aggregate
  • AggregateType - Type name of the aggregate

Event Access API Reference

After calling Result.Ok(tenant).WithEvents(eventData), callers can access events using a variety of extension methods.

Basic Event Extraction

GetEvents()

Returns all events as List<object>:

var result = Tenant.Create("Acme", "acme", "admin@acme.com");
var allEvents = result.GetEvents();  // List<object>

GetDomainEvents()

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}");
}

Strongly-Typed Event Extraction

GetEventsOfType<TEvent>()

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}");
}

Event Checking

HasEvents()

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());
}

HasEventsOfType<TEvent>()

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);
}

Common Usage Patterns

Pattern 1: Basic Event Processing

var result = Tenant.Create("Acme", "acme", "admin@acme.com");

if (result.IsSuccess && result.HasEvents())
{
    var events = result.GetDomainEvents();
    await _eventPublisher.PublishAsync(events);
}

Pattern 2: Type-Specific Event Handling

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);
    }
}

Pattern 3: Pattern Matching

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)
);

Pattern 4: Event Aggregation

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);

Pattern 5: API Controller Usage

[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 })
    );
}

Event Properties Available

All generated events implement IAggregateEvent and provide:

Common Properties (from IDomainEvent)

  • DateTime Timestamp - When the event occurred
  • string CorrelationId - Unique correlation identifier
  • string EventType - Type of event (e.g., "TenantCreateEvent")
  • int Version - Schema version for compatibility

Aggregate Properties (from IAggregateEvent)

  • string AggregateId - ID of the aggregate that generated the event
  • string AggregateType - Type name of the aggregate (e.g., "Tenant")

Generated Properties

  • Domain-specific properties based on method parameters
  • Aggregate properties (if configured with IncludeAggregateProperties)

Integration with Event Infrastructure

Event Store Integration

public async Task SaveTenantAsync(Tenant tenant, IEnumerable<IDomainEvent> events)
{
    await _tenantRepository.SaveAsync(tenant);
    await _eventStore.AppendEventsAsync(tenant.Id.ToString(), events);
}

Message Bus Integration

public async Task PublishEventsAsync(IEnumerable<IDomainEvent> events)
{
    foreach (var evt in events)
    {
        await _messageBus.PublishAsync(evt.EventType, evt);
    }
}

Read Model Updates

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.

Project Structure

The library consists of three components:

  • Mesch.DomainEvents - Main library with extensions and interfaces
  • Mesch.DomainEvents.Abstractions - Attributes and interfaces for source generator
  • Mesch.DomainEvents.SourceGenerator - Compile-time code generation (packaged as analyzer)

All components are included in the single Mesch.DomainEvents package.

Requirements

  • .NET 8.0 or later
  • C# 11.0 or later

Result Type

The library uses a custom Result<T> type that provides:

Type Safety

  • Compile-time enforcement of error handling
  • Pattern matching support via Match<TResult>() method
  • Implicit conversions from values and errors

API

  • Result<T>.Success(value) - Create successful result
  • Result<T>.Failure(error) - Create error result
  • result.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 safely
  • result.TryGetError(out error) - Try to get error safely
  • result.Match(onSuccess, onError) - Pattern match (2 overloads: returning TResult and void)

Benefits

  • 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
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.0.1 455 11/4/2025