Shared domain building blocks for DDD and Clean Architecture in .NET — entities, aggregates, value objects, domain events, and guards.
Entities and Aggregates — EntityBase<TId> and AggregateRoot<TId> base classes with identity, equality-by-ID, built-in audit fields, and domain event collection. Guid convenience aliases (EntityBase, AggregateRoot) auto-generate IDs.
Value Objects — ValueObject base class with structural equality. Subclasses define equality through GetEqualityComponents().
Domain Events — IDomainEvent contract and DomainEventBase record with auto-generated metadata (EventId, OccurredOn, EventType, Version). Supports with expressions for deterministic testing.
Contracts — IRepository<T, TId>, IUnitOfWork, IAuditableEntity, IAggregateRoot, ICurrentUserProvider, ICurrentTenantProvider, ITenantEntity, and IEventIdempotencyService. Pure abstractions — no infrastructure dependencies.
Guards — Static Guard class for argument validation and domain invariants. Uses [CallerArgumentExpression] to capture parameter names automatically.
Exceptions — DomainException for domain rule violations with an optional ErrorCode for structured error handling.
Extensions — ToSlug() string extension for URL-friendly slug generation with diacritics removal.
Inherit from EntityBase (Guid ID) or EntityBase<TId> (custom ID type). Audit fields and equality come built in.
public class Customer : EntityBase
{
public string Name { get; private set; }
public Customer(string name)
{
Guard.NotNullOrWhiteSpace(name);
Name = name;
}
}Inherit from AggregateRoot to get domain event support on top of entity features. Raise events inside domain methods — they queue until the unit of work dispatches them.
public class Order : AggregateRoot
{
public string CustomerName { get; private set; }
public OrderStatus Status { get; private set; }
public Order(string customerName)
{
Guard.NotNullOrWhiteSpace(customerName);
CustomerName = customerName;
Status = OrderStatus.Placed;
RaiseDomainEvent(new OrderPlaced { OrderId = Id });
}
public void Cancel()
{
Guard.Against(Status == OrderStatus.Shipped, "Cannot cancel a shipped order.");
Status = OrderStatus.Cancelled;
RaiseDomainEvent(new OrderCancelled { OrderId = Id });
}
}Use DomainEventBase as a record base. Metadata is auto-generated — override with with for testing.
public sealed record OrderPlaced : DomainEventBase
{
public Guid OrderId { get; init; }
}
// In a test with deterministic time:
var evt = new OrderPlaced { OrderId = id } with { OccurredOn = fixedTime };Subclass ValueObject and implement GetEqualityComponents(). Two value objects are equal when all components match.
public class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Guard.GreaterThanOrEqualTo(amount, 0m, "Amount cannot be negative.");
Guard.NotNullOrWhiteSpace(currency);
Amount = amount;
Currency = currency;
}
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
}Validate arguments and enforce domain invariants. Parameter names are captured automatically.
Guard.NotNull(customer); // throws ArgumentNullException
Guard.NotNullOrWhiteSpace(name); // throws ArgumentException
Guard.Against(age < 0, "Age cannot be negative."); // throws ArgumentException
Guard.Between(quantity, 1, 100, "Quantity out of range.");
Guard.MustBe(email, e => e.Contains('@'), "Invalid email.");
Guard.NotEmpty(lineItems); // throws on null or empty collectionDefine aggregate-specific repositories by extending IRepository<T> (Guid) or IRepository<T, TId> (custom ID):
public interface IOrderRepository : IRepository<Order>
{
Task<List<Order>> GetByCustomerAsync(string customerName, CancellationToken ct = default);
}The base interface provides GetByIdAsync, GetAllAsync, AddAsync, AddRangeAsync, UpdateAsync, UpdateRangeAsync, RemoveAsync, RemoveRangeAsync, and RemoveByIdAsync.
IUnitOfWork coordinates transactional persistence and domain event collection:
await unitOfWork.BeginTransactionAsync(ct);
await orderRepository.AddAsync(order, ct);
await unitOfWork.SaveChangesAsync(ct);
var events = unitOfWork.GetAndClearPendingEvents();
await unitOfWork.CommitTransactionAsync(ct);
// Dispatch events after commit
await dispatcher.NotifyAsync(events, ct);Mark aggregates or entities with ITenantEntity and resolve the current tenant with ICurrentTenantProvider:
public class Project : AggregateRoot, ITenantEntity
{
public Guid TenantId { get; private set; }
public Project(Guid tenantId, string name)
{
TenantId = tenantId;
Name = name;
}
}Throw DomainException for domain rule violations. The optional ErrorCode enables structured error mapping:
throw new DomainException("Order has already been shipped.");
throw new DomainException("Insufficient stock.", "INSUFFICIENT_STOCK");IEventIdempotencyService and ProcessedEvent prevent duplicate event handling:
if (await idempotencyService.HasBeenProcessedAsync(evt.EventId, nameof(MyHandler), ct))
return;
// Handle the event...
await idempotencyService.MarkAsProcessedAsync(evt.EventId, evt.EventType, nameof(MyHandler), ct);Convert strings to URL-friendly slugs with diacritics removal:
"Café au Lait".ToSlug() // "cafe-au-lait"
"Hello World!".ToSlug() // "hello-world"
"Ürban Köln".ToSlug() // "urban-koln"- Zero dependencies — depends only on the .NET base class library. No NuGet packages, no infrastructure concerns.
- Identity by ID — entities use equality-by-ID semantics. Value objects use structural equality.
- Audit built in — every entity tracks
CreatedAt,UpdatedAt,CreatedBy,UpdatedBythroughIAuditableEntity. - Events stay in the domain — aggregates raise events, the unit of work collects them, infrastructure dispatches them. The domain layer never depends on the dispatcher.
- Pure contracts —
IRepository,IUnitOfWork, and provider interfaces define what the domain needs without dictating how it's implemented. - Guard clauses over exceptions in constructors —
Guardkeeps validation expressive and consistent across the domain.
Demarbit.Shared.Domain ← this package (zero deps)
↑
Demarbit.Shared.Application ← dispatching, pipeline, validation
↑
Demarbit.Shared.Infrastructure ← EF Core, repositories, event dispatch
↑
[Your Application Project] ← references what it needs