Overview
Savvyio.Domain defines the DDD-facing model primitives in Savvy I/O. It builds on Savvyio.Core and gives you base types for entities, aggregate roots, value objects, domain events, and fire-and-forget domain event dispatch so a domain model can collect changes and expose them consistently.
The package stays focused on domain modeling concerns. It does not provide an event store or persistence implementation, but it does carry metadata with aggregates and events and includes the dispatcher and handler abstractions needed to raise pending domain events through the Savvy I/O pipeline.
Key APIs
Entity<TKey> is the base identity type for domain entities. It exposes the Id property with a protected setter and reports IsTransient when Id is still the default value for TKey, which tests verify with both default and assigned identifiers.
Aggregate<TKey, TEvent> builds on Entity<TKey> for aggregates that collect domain events. It exposes the read-only Events list, a Metadata dictionary, the protected AddEvent hook used by derived types to record changes, and RemoveAllEvents() to clear the pending event list.
AggregateRoot<TKey> is the package's DDD aggregate root specialization. It fixes the event type to IDomainEvent, so derived roots can focus on raising domain events without re-declaring the aggregate contract.
DomainEvent is the abstract record base for package-defined domain events. Its constructor always stores an event identifier and a UTC timestamp in metadata, and tests show that additional aggregate metadata can be merged into the event afterward.
DomainEventExtensions reads those metadata values back through GetEventId() and GetTimestamp(). These extension methods are the package-supported way to treat event identity and event time as part of the domain event contract instead of custom properties on every event type.
DomainEventDispatcher raises one IDomainEvent at a time through Savvy I/O's fire-and-forget dispatcher model. Raise() and RaiseAsync() both reject null requests and dispatch through registered IDomainEventHandler delegates resolved from the configured service locator.
DomainEventDispatcherExtensions publishes every pending event from an aggregate root. RaiseMany() and RaiseManyAsync() copy the aggregate's current Events, merge aggregate metadata into each event before dispatch, and tests show the common aggregate case ends with an empty event list after dispatch.
DomainEventHandler is the base class for class-based domain event handlers. Derived handlers override RegisterDelegates(IFireForgetRegistry<IDomainEvent>), and the constructor turns that registry callback into the Delegates activator consumed by the dispatcher.
ValueObject gives domain types value-based equality without hand-written comparison code. By default it uses all public readable properties with simple signatures, or nested ValueObject properties, as equality components, and tests cover both simple and nested value-object comparisons and hash-code behavior.
SingleValueObject<T> narrows ValueObject to a single Value member and seals equality to that one component. It also defines an implicit conversion back to T, which makes primitive-backed identifiers and scalar wrappers easier to consume from calling code.
SavvyioOptionsExtensions connects this package to Savvy I/O configuration. AddDomainEventHandler<TImplementation>() registers a domain event handler implementation, and AddDomainEventDispatcher() registers DomainEventDispatcher, which tests verify through the SavvyioOptions handler and dispatcher type collections.
Basic usage
using System;
using Codebelt.Extensions.Xunit;
using Savvyio.Domain;
using Xunit;
namespace MyProject.Tests;
public class ShipmentAggregateTests : Test
{
public ShipmentAggregateTests(ITestOutputHelper output) : base(output)
{
}
[Fact]
public void AggregateRoot_PendingEvents_CanClearCollection()
{
var before = DateTime.UtcNow;
var sut = new Shipment(Guid.NewGuid());
sut.Schedule();
var @event = Assert.IsType<ShipmentScheduled>(Assert.Single(sut.Events));
TestOutput.WriteLine($"Raised {nameof(ShipmentScheduled)} {@event.GetEventId()} at {@event.GetTimestamp():O}.");
Assert.Equal(sut.Id, @event.ShipmentId);
Assert.False(string.IsNullOrWhiteSpace(@event.GetEventId()));
Assert.InRange(@event.GetTimestamp(), before, DateTime.UtcNow.AddSeconds(1));
sut.RemoveAllEvents();
Assert.Empty(sut.Events);
}
}
public sealed class Shipment : AggregateRoot<Guid>
{
public Shipment(Guid id) : base(id)
{
}
public void Schedule() => AddEvent(new ShipmentScheduled(Id));
}
public sealed record ShipmentScheduled(Guid ShipmentId) : DomainEvent;
Use this pattern when an aggregate needs to record domain events as part of a state change and publish or persist them later in the application flow.
It matters because AggregateRoot<TKey> keeps identity, pending events, and event metadata conventions in the same domain model surface.
Installation
dotnet add package Savvyio.Domain
Usage guidance
Use Savvyio.Domain when your model benefits from explicit entities, value objects, aggregate roots, and domain events, and you want those concepts to participate in the same Savvy I/O metadata and dispatch pipeline. If you only need lower-level request and handler abstractions, Savvyio.Core is the narrower choice, and if you need an event-sourced aggregate root implementation rather than an in-memory pending event list, Savvyio.Domain.EventSourcing is the better fit.
Family packages
- 🏭Savvyio.App
- 📦Savvyio.Commands
- 📦Savvyio.Commands.Messaging
- 📦Savvyio.Core
- 📦Savvyio.Domain.EventSourcing
- 📦Savvyio.EventDriven
- 📦Savvyio.EventDriven.Messaging
- 📦Savvyio.Extensions.Dapper
- 📦Savvyio.Extensions.DapperExtensions
- 📦Savvyio.Extensions.DependencyInjection
- 📦Savvyio.Extensions.DependencyInjection.Dapper
- 📦Savvyio.Extensions.DependencyInjection.DapperExtensions
- 📦Savvyio.Extensions.DependencyInjection.Domain
- 📦Savvyio.Extensions.DependencyInjection.EFCore
- 📦Savvyio.Extensions.DependencyInjection.EFCore.Domain
- 📦Savvyio.Extensions.DependencyInjection.EFCore.Domain.EventSourcing
- 📦Savvyio.Extensions.DependencyInjection.NATS
- 📝Savvyio.Extensions.DependencyInjection.Newtonsoft.Json
- 📦Savvyio.Extensions.DependencyInjection.QueueStorage
- 📦Savvyio.Extensions.DependencyInjection.RabbitMQ
- 📦Savvyio.Extensions.DependencyInjection.SimpleQueueService
- 📝Savvyio.Extensions.DependencyInjection.Text.Json
- 📦Savvyio.Extensions.Dispatchers
- 📦Savvyio.Extensions.EFCore
- 📦Savvyio.Extensions.EFCore.Domain
- 📦Savvyio.Extensions.EFCore.Domain.EventSourcing
- 📦Savvyio.Extensions.NATS
- 📝Savvyio.Extensions.Newtonsoft.Json
- 📦Savvyio.Extensions.QueueStorage
- 📦Savvyio.Extensions.RabbitMQ
- 📦Savvyio.Extensions.SimpleQueueService
- 📝Savvyio.Extensions.Text.Json
- 📦Savvyio.Messaging
- 📦Savvyio.Queries