Aarkam.DDD.Domain
1.0.0-preview01
dotnet add package Aarkam.DDD.Domain --version 1.0.0-preview01
NuGet\Install-Package Aarkam.DDD.Domain -Version 1.0.0-preview01
<PackageReference Include="Aarkam.DDD.Domain" Version="1.0.0-preview01" />
<PackageVersion Include="Aarkam.DDD.Domain" Version="1.0.0-preview01" />
<PackageReference Include="Aarkam.DDD.Domain" />
paket add Aarkam.DDD.Domain --version 1.0.0-preview01
#r "nuget: Aarkam.DDD.Domain, 1.0.0-preview01"
#:package Aarkam.DDD.Domain@1.0.0-preview01
#addin nuget:?package=Aarkam.DDD.Domain&version=1.0.0-preview01&prerelease
#tool nuget:?package=Aarkam.DDD.Domain&version=1.0.0-preview01&prerelease
Aarkam.DDD.Domain
A high-performance, enterprise-grade, lightweight foundation package for Domain-Driven Design (DDD) and Event Sourcing (ES) patterns in modern .NET ecosystems.
Engineered with multi-target cross-runtime portability, zero-allocation micro-optimizations, and strict encapsulation semantics, Aarkam.DDD.Domain provides pristine abstractions without binding your domain core to concrete infrastructural dependenciesβinspired by the robust architectural elements of Volo.ABP Framework, MassTransit, and Marten.
π Table of Contents
- Quick Start
- Installation
- Core Concepts
- Architectural Patterns
- Usage Examples
- Performance Characteristics
- Security Considerations
- Comparison to Alternatives
- Contributing
- License
β‘ Quick Start (30 Seconds)
dotnet add package Aarkam.DDD.Domain
// Define a Value Object (immutable, equality-by-value)
public sealed class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
DomainGuard.AgainstNegative(amount, nameof(amount));
Amount = amount;
Currency = currency;
}
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
}
// Define an Entity (identity-based, mutable)
public class OrderItem : Entity<Guid>
{
public string ProductSku { get; private set; } = null!;
public Money Price { get; private set; } = null!;
public int Quantity { get; private set; }
protected OrderItem() { } // For EF Core
public OrderItem(Guid id, string sku, Money price, int quantity) : base(id)
{
DomainGuard.AgainstNullOrEmpty(sku, nameof(sku));
DomainGuard.AgainstNegativeOrZero(quantity, nameof(quantity));
ProductSku = sku;
Price = price;
Quantity = quantity;
}
}
// Define an Aggregate Root with Domain Events
public sealed class OrderPlacedEvent : DomainEvent<Order>
{
public string CustomerId { get; init; } = null!;
public decimal TotalValue { get; init; }
}
public class Order : AggregateRoot<Guid>
{
public string CustomerId { get; private set; } = null!;
public decimal TotalAmount { get; private set; }
public OrderStatus Status { get; private set; }
protected Order() { } // For EF Core
public Order(Guid id, string customerId) : base(id)
{
DomainGuard.AgainstNullOrEmpty(customerId, nameof(customerId));
RaiseEvent(new OrderPlacedEvent(id.ToString(), Version + 1, customerId, 0));
}
protected override void ApplyEvent(IDomainEvent domainEvent)
{
if (domainEvent is OrderPlacedEvent e)
{
Id = Guid.Parse(e.AggregateId);
CustomerId = e.CustomerId;
TotalAmount = e.TotalValue;
Status = OrderStatus.Created;
}
}
}
public enum OrderStatus { Created, Processing, Shipped, Completed }
That's it! You now have type-safe, fully-audited, event-sourced domain models.
π¦ Installation
Via .NET CLI
dotnet add package Aarkam.DDD.Domain
Via Package Manager
Install-Package Aarkam.DDD.Domain
Via NuGet Package Explorer
Search for Aarkam.DDD.Domain on nuget.org
Supported Platforms
.NET Standard 2.1(Xamarin, Unity, legacy .NET Framework).NET 6.0(LTS).NET 8.0(LTS).NET 10.0(Current)
ποΈ Core Concepts
1. Entity<TKey> β Thread of Continuity
An Entity is defined by its identity, not its attributes. Two entities with the same ID are considered the same, even if their attributes differ.
public abstract class Entity<TKey> : IEntity<TKey>, IEquatable<Entity<TKey>>
where TKey : IEquatable<TKey>
{
public virtual TKey Id { get; protected set; }
public virtual bool IsTransient() => EqualityComparer<TKey>.Default.Equals(Id, default);
}
Key Features:
- β Identity-based equality (not reference-based)
- β Transient detection (unpersisted entities)
- β ORM-friendly parameterless constructor
- β Protected setters guard against external mutation
2. ValueObject β Structural Immutability
A Value Object has no identity. Two value objects are completely interchangeable if their attributes match. They must be immutable.
public abstract class ValueObject : IEquatable<ValueObject>
{
protected abstract IEnumerable<object?> GetEqualityComponents();
public override int GetHashCode()
{
var hashCode = new HashCode();
foreach (var component in GetEqualityComponents())
hashCode.Add(component);
return hashCode.ToHashCode();
}
}
Key Features:
- β Attribute-based equality (structural comparison)
- β Works safely in HashSet<T> and Dictionary<K,V>
- β Immutability enforced via design
- β Composable (value objects can contain other value objects)
3. AggregateRoot<TKey> β Transactional Boundary
An Aggregate Root is the entry point for a cluster of entities. External code can only reference an aggregate via its root.
public abstract class AggregateRoot<TKey> : Entity<TKey>, IAggregateRoot<TKey>
{
private readonly List<IDomainEvent> _changes = [];
public virtual int Version { get; protected set; } = -1;
public virtual Dictionary<string, object?> ExtraProperties { get; protected set; }
public virtual bool IsDeleted { get; protected set; }
public virtual string? TenantId { get; protected set; }
protected void RaiseEvent(IDomainEvent domainEvent) => _changes.Add(domainEvent);
protected abstract void ApplyEvent(IDomainEvent domainEvent);
public IReadOnlyCollection<IDomainEvent> GetChanges() => _changes.AsReadOnly();
}
Key Features:
- β Event sourcing support (RaiseEvent + ApplyEvent pattern)
- β Multi-tenancy built-in (TenantId)
- β Soft deletes (IsDeleted flag + EF Global Query Filters)
- β Audit tracking (CreatorId, CreationTime, LastModifierId, LastModificationTime)
- β Extensible properties (ExtraProperties for dynamic attributes)
- β Optimistic concurrency (Version field for race condition detection)
- β Zero-copy event collection (GetChanges returns direct list cast)
4. DomainGuard β Fail-Fast Validation
Guards prevent garbage collection (GC) thrashing via compiled lambda exception factories.
public static class DomainGuard
{
public static void AgainstNull([NotNull] object? value, string parameterName);
public static void AgainstNullOrEmpty([NotNull] string? value, string parameterName);
public static void AgainstNegative(long value, string parameterName);
public static void AgainstNegativeOrZero(long value, string parameterName);
public static void AgainstOutOfRange(int value, int min, int max, string parameterName);
public static void CheckRule(IBusinessRule rule);
}
Performance: Uses compiled expression factories instead of Activator.CreateInstance(), eliminating reflection overhead.
5. IBusinessRule β Complex Invariants
Encapsulate multi-field business logic without polluting the aggregate with conditional chains.
public interface IBusinessRule
{
bool IsBroken();
string Message { get; }
}
public sealed class CustomerMustHaveValidEmailRule : IBusinessRule
{
private readonly string _email;
public CustomerMustHaveValidEmailRule(string email) => _email = email;
public bool IsBroken() => !_email.Contains("@");
public string Message => "Email format is invalid.";
}
// Usage
public void UpdateEmail(string newEmail)
{
DomainGuard.AgainstNullOrEmpty(newEmail, nameof(newEmail));
DomainGuard.CheckRule(new CustomerMustHaveValidEmailRule(newEmail));
Email = newEmail;
}
6. IDomainEvent β Immutable Change Facts
Events are the single source of truth for state changes. They are append-only and immutable.
public interface IDomainEvent
{
string AggregateId { get; }
int Version { get; }
DateTime OccurredAt { get; }
Dictionary<string, object>? Metadata { get; }
}
public abstract class DomainEvent<TAggregate> : IDomainEvent
where TAggregate : AggregateRoot<Guid>
{
public string AggregateId { get; init; } = null!;
public int Version { get; init; }
public DateTime OccurredAt { get; init; } = DateTime.UtcNow;
public Dictionary<string, object>? Metadata { get; init; }
}
7. ISpecification<T> β Translatable Queries
Build complex queries that translate cleanly to SQL via EF Core.
public sealed class ActiveOrdersSpecification : SpecificationBase<Order>
{
public ActiveOrdersSpecification(string customerId, int pageIndex = 0, int pageSize = 10)
{
AddInclude(o => o.Items);
AddInclude(o => o.Payments);
OrderByDescending = o => o.CreationTime;
Skip = pageIndex * pageSize;
Take = pageSize;
}
public override Expression<Func<Order, bool>> ToExpression()
{
return order => order.CustomerId == customerId
&& !order.IsDeleted
&& order.Status != OrderStatus.Completed;
}
}
// Usage in Repository
public async Task<List<Order>> GetActiveOrdersAsync(string customerId)
{
var spec = new ActiveOrdersSpecification(customerId);
return await _context.Orders
.Where(spec.ToExpression())
.ToListAsync();
}
ποΈ Architectural Patterns Reference
| Component | DDD Pattern | Use Case |
|---|---|---|
Entity<TKey> |
Thread of Continuity | Objects with persistent identity |
ValueObject |
Structural Immutability | Money, Address, Email, Coordinates |
AggregateRoot<TKey> |
Transactional Boundary | Order, Customer, Invoice |
IDomainEvent |
Append-Only Facts | Order Placed, Payment Received |
IBusinessRule |
Invariant Validation | Email must be unique, quantity > 0 |
ISpecification<T> |
Query Translation | Filtered searches, pagination |
IDomainService |
Stateless Cross-Aggregate | Calculating discounts, validating policies |
IInboxMessage / IOutboxMessage |
Transactional Messaging | Idempotent message handling |
ISnapshot |
State Baseline Rehydration | Optimizing event stream replays |
ISaga<TSagaData> |
Distributed Orchestration | Long-running workflows |
Reference Literature:
- Eric Evans, Domain-Driven Design (Evans, 2003)
- Vaughn Vernon, Implementing Domain-Driven Design (Vernon, 2013)
- Greg Young, CQRS and Event Sourcing (Young, 2010)
- Martin Fowler, Patterns of Enterprise Application Architecture (Fowler, 2002)
- Gregor Hohpe, Enterprise Integration Patterns (Hohpe & Woolf, 2003)
π» Usage Examples
Example 1: E-Commerce Order Management
namespace Ecommerce.Orders.Domain;
// Value Objects
public sealed class OrderNumber : ValueObject
{
public string Value { get; }
public OrderNumber(string value)
{
DomainGuard.AgainstNullOrEmpty(value, nameof(value));
Value = value;
}
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Value;
}
}
public sealed class OrderLineItem : ValueObject
{
public string Sku { get; }
public int Quantity { get; }
public Money UnitPrice { get; }
public OrderLineItem(string sku, int quantity, Money unitPrice)
{
DomainGuard.AgainstNullOrEmpty(sku, nameof(sku));
DomainGuard.AgainstNegativeOrZero(quantity, nameof(quantity));
DomainGuard.AgainstNull(unitPrice, nameof(unitPrice));
Sku = sku;
Quantity = quantity;
UnitPrice = unitPrice;
}
public Money GetLineTotal() => new(UnitPrice.Amount * Quantity, UnitPrice.Currency);
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Sku;
yield return Quantity;
yield return UnitPrice;
}
}
// Domain Events
public sealed class OrderCreatedEvent : DomainEvent<Order>
{
public string CustomerId { get; init; } = null!;
public List<OrderLineItem> LineItems { get; init; } = [];
public Money TotalAmount { get; init; } = null!;
}
public sealed class OrderConfirmedEvent : DomainEvent<Order>
{
public string ConfirmedBy { get; init; } = null!;
public DateTime ConfirmedAt { get; init; }
}
public sealed class OrderCancelledEvent : DomainEvent<Order>
{
public string CancellationReason { get; init; } = null!;
}
// Aggregate Root
public class Order : AggregateRoot<Guid>
{
private readonly List<OrderLineItem> _lineItems = [];
public OrderNumber OrderNumber { get; private set; } = null!;
public string CustomerId { get; private set; } = null!;
public Money TotalAmount { get; private set; } = null!;
public OrderStatus Status { get; private set; }
public IReadOnlyList<OrderLineItem> LineItems => _lineItems.AsReadOnly();
protected Order() { } // For EF Core
public Order(Guid id, OrderNumber orderNumber, string customerId) : base(id)
{
DomainGuard.AgainstNull(orderNumber, nameof(orderNumber));
DomainGuard.AgainstNullOrEmpty(customerId, nameof(customerId));
OrderNumber = orderNumber;
CustomerId = customerId;
Status = OrderStatus.Draft;
RaiseEvent(new OrderCreatedEvent
{
AggregateId = id.ToString(),
Version = Version + 1,
CustomerId = customerId
});
}
public void AddLineItem(OrderLineItem lineItem)
{
DomainGuard.Against<InvalidOperationException>(
Status != OrderStatus.Draft,
"Cannot add items to a confirmed order."
);
DomainGuard.AgainstNull(lineItem, nameof(lineItem));
_lineItems.Add(lineItem);
RecalculateTotal();
}
public void Confirm(string confirmedBy)
{
DomainGuard.AgainstNullOrEmpty(confirmedBy, nameof(confirmedBy));
DomainGuard.Against<InvalidOperationException>(
_lineItems.Count == 0,
"Cannot confirm an empty order."
);
Status = OrderStatus.Confirmed;
RaiseEvent(new OrderConfirmedEvent
{
AggregateId = Id.ToString(),
Version = Version + 1,
ConfirmedBy = confirmedBy,
ConfirmedAt = DateTime.UtcNow
});
}
public void Cancel(string reason)
{
DomainGuard.AgainstNullOrEmpty(reason, nameof(reason));
DomainGuard.Against<InvalidOperationException>(
Status == OrderStatus.Completed,
"Cannot cancel a completed order."
);
Status = OrderStatus.Cancelled;
RaiseEvent(new OrderCancelledEvent
{
AggregateId = Id.ToString(),
Version = Version + 1,
CancellationReason = reason
});
}
protected override void ApplyEvent(IDomainEvent domainEvent)
{
switch (domainEvent)
{
case OrderCreatedEvent e:
OrderNumber = new(e.AggregateId);
CustomerId = e.CustomerId;
TotalAmount = e.TotalAmount;
Status = OrderStatus.Draft;
break;
case OrderConfirmedEvent:
Status = OrderStatus.Confirmed;
break;
case OrderCancelledEvent:
Status = OrderStatus.Cancelled;
break;
}
}
private void RecalculateTotal()
{
var total = _lineItems.Sum(li => li.GetLineTotal().Amount);
TotalAmount = new(total, "USD");
}
}
public enum OrderStatus { Draft, Confirmed, Processing, Shipped, Completed, Cancelled }
Example 2: Multi-Tenant SaaS Application
// Inject current tenant from authentication middleware
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly ICurrentTenant _currentTenant;
public OrderService(IOrderRepository repository, ICurrentTenant currentTenant)
{
_repository = repository;
_currentTenant = currentTenant;
}
public async Task CreateOrderAsync(CreateOrderRequest request)
{
var order = new Order(Guid.NewGuid(), request.OrderNumber, request.CustomerId);
order.TenantId = _currentTenant.Id; // Automatically scoped to tenant
await _repository.AddAsync(order);
await _repository.SaveChangesAsync();
}
// Tenant context switching for background jobs
public async Task ProcessCrossTenantSyncAsync(string targetTenantId, Guid orderId)
{
using (_currentTenant.Change(targetTenantId))
{
var order = await _repository.GetAsync(orderId);
order.Confirm("system");
await _repository.UpdateAsync(order);
}
}
}
β‘ Performance Characteristics
Zero-Allocation Optimizations
GetChanges() Smart Cast
public IReadOnlyCollection<IDomainEvent> GetChanges() => _changes.AsReadOnly(); // Direct cast, no copyImpact: Eliminates allocation when collecting uncommitted events.
Compiled Exception Factories
private static class ExceptionFactory<TException> where TException : Exception { private static readonly Func<string, TException> _factory = CreateFactory(); public static TException Create(string message) => _factory(message); }Impact: Guard clauses don't trigger reflection overhead; useful in tight loops.
ValueObject HashCode Computation
- Uses modern
HashCodestruct (low collision rates) - Avoids legacy prime-multiplication algorithms
- Safe for use in HashSet<T> and Dictionary<K,V>
- Uses modern
Benchmarks (Estimated)
| Operation | Time | Allocations |
|---|---|---|
| Entity.Equals() | <1 Β΅s | 0 bytes |
| ValueObject.Equals() | 1-5 Β΅s | 0 bytes |
| DomainGuard.AgainstNull() | <1 Β΅s | 0 bytes (cached lambda) |
| Order.RaiseEvent() | <1 Β΅s | 64 bytes (event object) |
Note: Benchmarks are illustrative. Run dotnet benchmark for precise measurements in your environment.
π Security Considerations
1. Tenant Isolation
Risk: Accidental cross-tenant data leakage
Mitigation:
// β
CORRECT: Inject TenantId from authenticated context
public void Create(CreateOrderRequest request)
{
var order = new Order(Guid.NewGuid(), request.OrderNumber, request.CustomerId);
order.TenantId = _currentTenant.Id; // Set from middleware, not user input
_repository.Add(order);
}
// β WRONG: TenantId from user input
public void Create(CreateOrderRequest request)
{
var order = new Order(Guid.NewGuid(), request.OrderNumber, request.CustomerId);
order.TenantId = request.TenantId; // SECURITY BUG: trusting user input!
}
Best Practice: Use ICurrentTenant middleware that derives TenantId from JWT claims or session.
2. Entity Serialization
Risk: Sensitive data exposed via JSON serialization
Mitigation:
public class Order : AggregateRoot<Guid>
{
[JsonIgnore]
public string? InternalNotes { get; private set; }
[JsonIgnore]
public Dictionary<string, object?> ExtraProperties { get; protected set; }
}
3. Guard Clause Validation
Always validate at aggregate boundaries:
public void UpdatePrice(Money newPrice)
{
DomainGuard.AgainstNull(newPrice, nameof(newPrice));
DomainGuard.AgainstNegative(newPrice.Amount, nameof(newPrice.Amount));
// Only here is the price safe to mutate
Price = newPrice;
}
π― Comparison to Alternatives
vs. Volo.ABP Framework
| Aspect | Aarkam.DDD.Domain | Volo.ABP |
|---|---|---|
| Dependency Injection | 0 (pure types) | Full integration |
| Database Abstraction | None (works with any ORM) | Built-in repositories |
| Authorization | 0 (leave to middleware) | RBAC + ABP.Authorization |
| Performance | Optimized (zero-allocation) | Full-featured (some overhead) |
| Learning Curve | Minimal | Steep (more to learn) |
| Use Case | Foundation library | Complete framework |
When to use Aarkam: You want DDD patterns without framework lock-in.
When to use ABP: You need a complete, opinionated platform.
vs. MassTransit
| Aspect | Aarkam.DDD.Domain | MassTransit |
|---|---|---|
| Purpose | Domain modeling | Message orchestration |
| Event Sourcing | Built-in contracts | External (via Marten) |
| Sagas | Interface defined | Fully implemented |
| Message Bus | 0 (BYOB) | Integrated |
Complementary: Use both! Aarkam for domain, MassTransit for distribution.
vs. Marten
| Aspect | Aarkam.DDD.Domain | Marten |
|---|---|---|
| Database | Agnostic (any DB) | PostgreSQL only |
| Event Store | Contracts only | Full implementation |
| ACID Snapshots | Via user code | Built-in |
| Query Optimization | EF Core specs | Marten projections |
Complementary: Use Aarkam for domain types, Marten for PostgreSQL event store backend.
π€ Contributing
Contributions are welcome! Please follow these guidelines:
- Report issues via GitHub Issues (include reproduction steps)
- Fork and branch from
developbranch - Write tests for new features (xUnit)
- Follow code style (nullable annotations, XML docs, 120-char lines)
- Commit messages in conventional format:
feat: add ISaga interface - Submit PR with description and linked issue
Development Setup:
git clone https://github.com/melhelbawi/Aarkam.DDD.Domain.git
cd Aarkam.DDD.Domain
dotnet restore
dotnet test
dotnet build
π Versioning
This project follows Semantic Versioning 2.0.0:
- MAJOR (X.0.0): Breaking API changes
- MINOR (0.X.0): New features, backward-compatible
- PATCH (0.0.X): Bug fixes, no API changes
Stability:
1.0.0+: Production-ready0.x.x: Preview (API may change)
π License
Distributed under the MIT License. See LICENSE for details.
Copyright (c) 2026 Mohamed Elhelbawi / Aarkam
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions...
π Acknowledgments
Built with inspiration from:
- Eric Evans β Domain-Driven Design (Blue Book)
- Vaughn Vernon β Implementing DDD
- Greg Young β Event Sourcing Patterns
- Martin Fowler β Enterprise Architecture Patterns
- Volo.ABP Team β ABP Framework design philosophy
- MassTransit Contributors β Distributed systems patterns
π Support
- Documentation: GitHub Wiki
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Made with β€οΈ by Mohamed Elhelbawi
π Further Reading
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. 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 is compatible. 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. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- System.ComponentModel.Annotations (>= 5.0.0)
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.9)
- System.ComponentModel.Annotations (>= 5.0.0)
-
net6.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- System.Collections.Immutable (>= 8.0.0)
-
net8.0
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 |
|---|---|---|
| 1.0.0-preview01 | 85 | 6/27/2026 |