ThirteenBytes.DDDPatterns.Primitives
1.2.0
dotnet add package ThirteenBytes.DDDPatterns.Primitives --version 1.2.0
NuGet\Install-Package ThirteenBytes.DDDPatterns.Primitives -Version 1.2.0
<PackageReference Include="ThirteenBytes.DDDPatterns.Primitives" Version="1.2.0" />
<PackageVersion Include="ThirteenBytes.DDDPatterns.Primitives" Version="1.2.0" />
<PackageReference Include="ThirteenBytes.DDDPatterns.Primitives" />
paket add ThirteenBytes.DDDPatterns.Primitives --version 1.2.0
#r "nuget: ThirteenBytes.DDDPatterns.Primitives, 1.2.0"
#:package ThirteenBytes.DDDPatterns.Primitives@1.2.0
#addin nuget:?package=ThirteenBytes.DDDPatterns.Primitives&version=1.2.0
#tool nuget:?package=ThirteenBytes.DDDPatterns.Primitives&version=1.2.0
ThirteenBytes.DDDPatterns.Primitives
A comprehensive library of Domain-Driven Design (DDD) primitives and patterns for .NET applications. This package provides essential building blocks for implementing DDD principles, including aggregate roots, entities, value objects, domain events, and repository abstractions.
Domain-Driven Design is an approach to software development that:
- Centers the domain model: Software should reflect the business’s real concepts, rules, and language.
- Uses Ubiquitous Language: A shared vocabulary across developers and business experts that’s embedded in code.
- Defines boundaries (Bounded Contexts): Each model has a clear boundary where its definitions are consistent and cohesive.
- Provides tactical building blocks: Entities (with identity), Value Objects (immutable), Aggregates (consistency boundaries), Repositories, Services, and Factories.
- Strategic design: Distinguishes between Core Domains (where innovation happens) and Supporting/Generic subdomains (where reuse or simpler solutions may suffice).
- Pragmatism: DDD is most useful in complex, evolving domains, where deep collaboration between domain experts and developers is necessary to avoid chaos.
Basic Definitions
- Bounded Context: Boundary where a model has a single, consistent meaning.
- Aggregate Root: The entry point to an Aggregate, enforces invariants, consistency rules.
- Entity: Object defined by identity that persists through state changes.
- Value Object: Object defined only by attributes, immutable, no identity.
Detailed Definitions:
Bounded Context
- Eric Evans (DDD): A Bounded Context is an explicit boundary within which a particular model applies. The meaning of terms, rules, and entities are consistent only inside that boundary. Outside, the same words may mean different things, so you must define translations or integrations.
- Vaughn Vernon (IDDD): A Bounded Context is the guardrail that keeps a model coherent. It’s both a linguistic and technical boundary, ensuring that terms have unambiguous meaning and implementations don’t drift into corruption. Teams can evolve independently within their bounded contexts.
- Martin Fowler: A Bounded Context ensures that the model is not spread too thin. It provides clarity by stating “this is where this model applies, and outside we don’t make assumptions.” It’s crucial for large systems to avoid “semantic diffusion” (where words lose meaning).
Aggregate Root (and Aggregates)
- Eric Evans: An Aggregate is a cluster of associated objects treated as a unit for data changes. It enforces invariants across its members. One Entity is designated the Aggregate Root, and only the root is accessible from outside—other members are reached through it.
- Vaughn Vernon: Aggregates are the transactional consistency boundaries. The Aggregate Root protects the integrity of the aggregate and enforces invariants. Clients only hold references to the root, never to its children directly.
- Martin Fowler: Aggregates simplify transactional rules. Rather than every entity enforcing consistency globally, you encapsulate rules inside an Aggregate and enforce them there, making it clear what must be consistent together.
Entities
- Eric Evans: An Entity is an object distinguished by its identity, not just by its attributes. Two Entities with the same attributes but different identities are not the same. Identity runs through its entire lifecycle.
- Vaughn Vernon: Entities model concepts that endure over time. Their identity is explicit (often via a unique ID), and their attributes and associations can change, but they remain the same entity.
- Martin Fowler: In enterprise applications, Entities represent things like “Customer” or “Order” where who it is matters more than its current data. Identity is fundamental, not just the data snapshot.
Value Objects
- Eric Evans: A Value Object is an object that is defined only by its attributes, not by a distinct identity. They are immutable and replaceable. For example, two Money objects with the same amount and currency are indistinguishable.
- Vaughn Vernon: Value Objects should be immutable and side-effect free. They are often used to model concepts like measurements, addresses, or date ranges. They can encapsulate logic (e.g., currency conversions) but should never have identity.
- Martin Fowler: A Value Object represents descriptive aspects of the domain. They make code more expressive and reduce complexity by avoiding unnecessary identity. They’re safer because immutability eliminates unintended side effects.
Features
- Strongly-Typed Identifiers: Type-safe entity IDs supporting various underlying types (Guid, string, Ulid, etc.)
- Entity Base Classes: Abstract base classes for entities with identity-based equality
- Audit Support: Built-in audit tracking with creation and modification timestamps
- Soft Delete Support: IDeletable interface and DeletableAuditEntity base class for logical deletion
- Value Objects: Base classes for immutable value objects with structural equality
- Aggregate Roots: Event-sourcing capable aggregate roots with domain event management
- Domain Events: Interfaces and base classes for domain event implementation
- Repository Pattern: Generic repository interface for data access abstraction
- Event Store Abstraction: Interface for event sourcing persistence
- Result Pattern: Functional error handling without exceptions
- Pagination Support: Built-in pagination for event streams and query results
Quick Start
1. Define a Strongly-Typed ID
public record UserId : EntityId<UserId>
{
public Guid Value { get; }
private UserId(Guid value) => Value = value;
public static UserId New() => new(Guid.NewGuid());
public static UserId From(Guid value) => new(value);
}
public sealed record TaskId(Guid Value) : EntityId<Guid>(Value)
{
public static TaskId New() =>
new(Guid.NewGuid());
public static TaskId From(Guid value) =>
new(value);
}
2. Create a Value Object
public class TaskName : ValueObject<string, TaskName>
{
public static Result<TaskName> Create(string value)
{
return WithValidation(
value,
Validate,
value => new TaskName(value)
);
}
private static List<Error> Validate(string value)
{
var errors = new List<Error>();
if (string.IsNullOrWhiteSpace(value))
{
errors.Add(Error.Validation("Task name cannot be empty."));
}
if (value.Length > 100)
{
errors.Add(Error.Validation("Task name cannot exceed 100 characters."));
}
return errors;
}
}
3. Create an Entity
public class User : Entity<UserId>
{
public UserId Id { get; private set; }
public Email Email { get; private set; } // Using the previously defined Value Object
// Private constructor for ORM
private User() { }
private User(UserId id, Email email)
{
Id = id;
Email = email;
}
public static Result<User> Create(UserId id, string email)
{
return WithValidation(
() => Email.Create(email), // Validate and create Email value object
emailObj => new User(id, emailObj.Value)); // Create User with validated Email
}
}
4. Create a Soft Deletable Entity
// Entity that supports soft delete
public class Photo : DeletableAuditEntity<PhotoId>
{
public string Url { get; private set; }
public string BlobReference { get; private set; }
private Photo() { } // EF Core
private Photo(PhotoId id, string url, string blobRef) : base(id)
{
Url = url;
BlobReference = blobRef;
}
public static Photo Create(PhotoId id, string url, string blobRef)
=> new(id, url, blobRef);
}
// Usage - Soft Delete
photo.MarkAsDeleted(DateTime.UtcNow);
await dbContext.SaveChangesAsync();
// Usage - Restore (undo/compensation)
photo.Restore();
await dbContext.SaveChangesAsync();
// EF Core Configuration
builder.HasQueryFilter(e => e.DeletedAtUtc == null); // Auto-exclude deleted
builder.HasIndex(e => e.DeletedAtUtc); // Optimize cleanup queries
// Query Extensions
var activePhotos = await dbContext.Photos
.ExcludeDeleted() // Explicitly exclude soft deleted
.ToListAsync();
var deletedPhotos = await dbContext.Photos
.IgnoreQueryFilters()
.OnlyDeleted() // Only soft deleted entities
.Where(p => p.DeletedAtUtc <= cutoffDate)
.ToListAsync();
5. Create an Aggregate Root with Domain Events
// Domain Events
public record UserRegistered(UserId UserId, string Name, string Email) : DomainEvent;
public record EmailChanged(UserId UserId, string NewEmail) : DomainEvent;
// Aggregate Root
public class UserAccount : AggregateRoot<UserId>
{
public string Name { get; private set; }
public Email Email { get; private set; }
// EF Core constructor - register event handlers here
private UserAccount()
{
On<UserRegistered>(When);
On<EmailChanged>(When);
}
public UserAccount(UserId id, string name, Email email) : this()
{
var @event = new UserRegistered(id, name, email.Value);
Apply(@event);
}
public Result ChangeEmail(Email newEmail)
{
var @event = new EmailChanged(Id, newEmail.Value);
return Apply(@event);
}
// Event handlers - pure state mutation, no validation
private void When(UserRegistered @event)
{
Name = @event.Name;
Email = Email.Create(@event.Email).ValueOrThrow(); // Safe since validation happened in command
}
private void When(EmailChanged @event)
{
Email = Email.Create(@event.NewEmail).ValueOrThrow(); // Safe since validation happened in command
}
}
6. Use the Repository Pattern
public class UserService
{
private readonly IRepository<User, UserId, Guid> _repository;
private readonly IUnitOfWork _unitOfWork;
public UserService(IRepository<User, UserId, Guid> repository, IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}
public async Task<Result<User>> CreateUserAsync(string name, string email)
{
var emailResult = Email.Create(email);
if (emailResult.IsFailure)
return emailResult.Errors;
var user = new User(UserId.New(), name, emailResult.Value);
await _repository.AddAsync(user);
await _unitOfWork.SaveChangesAsync();
return user;
}
public async Task<Result<User>> UpdateUserEmailAsync(Guid userId, string newEmail)
{
var id = UserId.From(userId);
var user = await _repository.GetByIdAsync(id);
if (user == null)
return Error.NotFound("User not found");
var emailResult = Email.Create(newEmail);
if (emailResult.IsFailure)
return emailResult.Errors;
var updateResult = user.UpdateEmail(emailResult.Value);
if (updateResult.IsFailure)
return updateResult.Errors;
await _repository.UpdateAsync(user);
await _unitOfWork.SaveChangesAsync();
return user;
}
}
7. Event Sourcing with Event Store
public class UserAccountService
{
private readonly IEventStore _eventStore;
public UserAccountService(IEventStore eventStore)
{
_eventStore = eventStore;
}
public async Task<Result<UserAccount>> CreateAccountAsync(string name, string email)
{
var emailResult = Email.Create(email);
if (emailResult.IsFailure)
return emailResult.Errors;
var account = new UserAccount(UserId.New(), name, emailResult.Value);
// Save events for new aggregate (version 0)
await account.SaveNewAggregateEventsAsync(_eventStore);
return account;
}
public async Task<Result<UserAccount>> ChangeEmailAsync(Guid userId, string newEmail)
{
var id = UserId.From(userId);
// Load aggregate from event stream
var events = await _eventStore.GetEventsAsync<UserId, Guid>(id);
if (!events.Any())
return Error.NotFound("User account not found");
var account = new UserAccount(); // Create empty aggregate
var replayResult = account.Replay(events); // Rebuild state from events
if (replayResult.IsFailure)
return replayResult.Errors;
// Perform business operation
var emailResult = Email.Create(newEmail);
if (emailResult.IsFailure)
return emailResult.Errors;
var changeResult = account.ChangeEmail(emailResult.Value);
if (changeResult.IsFailure)
return changeResult.Errors;
// Save new events
var currentVersion = await _eventStore.GetAggregateVersionAsync<UserId, Guid>(id);
await account.SaveEventsAsync(_eventStore, currentVersion);
return account;
}
public async Task<PagedResult<IDomainEvent>> GetAccountHistoryAsync(Guid userId, int pageNumber = 1, int pageSize = 10)
{
var id = UserId.From(userId);
return await _eventStore.GetEventsPagedAsync<UserId, Guid>(id, pageNumber, pageSize);
}
}
8. Result Pattern for Error Handling
public async Task<Result<UserAccount>> ProcessUserRegistration(string name, string email)
{
// Validate email
var emailResult = Email.Create(email); if (emailResult.IsFailure) return emailResult.Errors;
// Check if user already exists
var existingUser = await _repository.GetAsync(u => u.Email.Value == email);
if (existingUser != null)
{
return Error.Conflict("User with this email already exists");
}
// Create and save user
var account = new UserAccount(UserId.New(), name, emailResult.Value!);
var saveResult = await account.SaveNewAggregateEventsAsync(_eventStore);
if (saveResult.IsFailure)
{
return saveResult.Errors;
}
return account;
}
Best Practices
- Always validate in value object factories - Use the
Createpattern with validation - Keep aggregates small - Focus on transaction boundaries
- Use Result pattern consistently - Avoid exceptions for business logic failures
- Register event handlers in parameterless constructor - For event sourcing reconstruction
- Implement proper equality - Override GetHashCode and Equals for value objects
- Use strongly-typed IDs - Prevent ID mixups and improve type safety
- Separate commands from queries - Follow CQRS principles
- Make value objects immutable - Ensure thread safety and predictable behavior
Real-World Examples
This library includes comprehensive examples demonstrating real-world usage:
- Example 1: In-memory implementation with simple CRUD operations and event sourcing
- Example 2: Advanced implementation with SQLite persistence and RavenDB event store
Key features demonstrated in the examples:
- Bank account management with money deposits/withdrawals
- Account holder management with validation
- Event sourcing with event replay
- CQRS pattern implementation
- Clean Architecture structure
- Unit testing patterns
Check the /Examples folder in the source repository for complete working applications.
API Reference
Core Interfaces
IEntity<TId>- Base entity contractIAuditEntity- Audit tracking contractIDeletable- Soft delete contractIAggregateRoot- Aggregate root with event managementIRepository<T, TId>- Repository pattern interfaceIEventStore- Event sourcing persistenceIUnitOfWork- Transaction managementIDomainEventDispatcher- Event publishing
Base Classes
EntityId<TValue>- Abstract base for strongly-typed identifiersEntity<TId>- Entity with identity-based equalityAuditEntity<TId>- Entity with audit timestampsDeletableAuditEntity<TId>- Entity with audit timestamps and soft delete supportAggregateRoot<TId>- Event-sourced aggregate rootValueObject/ValueObject<TValue, TSelf>- Immutable value objects with structural equalityDomainEvent- Base record for domain events
Utility Classes
Result/Result<T>- Functional error handlingError- Standardized error representationPagedResult<T>- Pagination support
Requirements
- .NET 10.0 or higher
- C# 14+ (for primary constructors and modern syntax)
Contributing
Contributions are welcome! Please feel free to submit issues and pull requests to the GitHub repository.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
For questions, issues, or feature requests, please visit the GitHub repository or contact the maintainers.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. |
-
net10.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 |
|---|---|---|
| 1.2.0 | 101 | 2/10/2026 |
| 1.1.3 | 100 | 2/1/2026 |
| 1.1.1 | 217 | 9/13/2025 |
| 1.1.0 | 120 | 9/12/2025 |
| 1.1.0-beta0001 | 130 | 9/12/2025 |
| 1.0.0 | 203 | 9/12/2025 |
| 0.1.0-release0001 | 195 | 9/12/2025 |