DomainBase 2.0.0
dotnet add package DomainBase --version 2.0.0
NuGet\Install-Package DomainBase -Version 2.0.0
<PackageReference Include="DomainBase" Version="2.0.0" />
<PackageVersion Include="DomainBase" Version="2.0.0" />
<PackageReference Include="DomainBase" />
paket add DomainBase --version 2.0.0
#r "nuget: DomainBase, 2.0.0"
#:package DomainBase@2.0.0
#addin nuget:?package=DomainBase&version=2.0.0
#tool nuget:?package=DomainBase&version=2.0.0
DomainBase
Lightweight, pragmatic building blocks for Domain-Driven Design (DDD) in .NET: entities, aggregate roots, value objects, domain events, specifications, repositories, and more. Includes source generators and analyzers to keep your domain model clean, safe, and fast.
Table of contents
Why DomainBase
- Clear, minimal primitives:
Entity<TId>
,AggregateRoot<TId>
,ValueObject<TSelf>
,Enumeration
,DomainEvent
,Specification<T>
- Batteries included: analyzers (diagnostics + code fixes) and source generators that eliminate boilerplate
- AOT-friendly and fast: optimized equality, no runtime code emission
- Production-minded: exceptions, auditing interfaces, domain event dispatcher
Install
dotnet add package DomainBase
Targets: net9.0
(generators/analyzers: netstandard2.0
).
DDD primer
- Entity: Has identity (
Id
) and a lifecycle. Example:Order
,Customer
. - Value object: Identity-less, immutable values compared by their content. Example:
Money
,Email
. - Aggregate & aggregate root: A cluster of entities and value objects that change together. The root (e.g.,
Order
) is the only entry point. The aggregate is your transactional consistency boundary: enforce invariants inside it and commit changes atomically. - Domain event: Something significant that happened in the domain (e.g.,
OrderSubmitted
). Raised by the aggregate root and handled asynchronously. - Specification: Reusable query logic.
- Repository: Abstraction for loading/saving aggregates.
- Domain service: Domain behavior that doesn’t belong on a particular entity/value object.
Types and rules
Entities and aggregate roots
- What they are: Objects with identity (
Id
). Aggregates group related entities/value objects and can raise domain events. - Use when: The thing has a life-cycle and identity (e.g.,
Order
,Customer
). - Rules:
- Compare by
Id
. - The aggregate is a transactional consistency boundary. Keep invariants inside; only the root exposes behaviors that mutate state.
- Use
AddDomainEvent
on the root to record significant changes; dispatch and clear after saving.
- Compare by
public sealed class Order : AggregateRoot<Guid>
{
public Order(Guid id) : base(id) { }
public bool Submitted { get; private set; }
public void Submit() { if (Submitted) return; Submitted = true; AddDomainEvent(new OrderSubmitted(Id)); }
}
public sealed record OrderSubmitted(Guid OrderId) : DomainEvent;
Value objects
- What they are: Immutable values compared by their content (not identity).
- Three ways to declare equality behaviors (more exmplanation in the Guide section):
- Wrapper:
ValueObject<TSelf,TValue>
. This is used when the value object has only 1 single value wrapped within it. - Manual:
ValueObject<TSelf>
+ your ownEqualsCore
/GetHashCodeCore
. This is the default VO initializer (although the other ones are recommended in most cases) - Generator: Adding partial flag to the value object with
[ValueObject]
and attributes. This will trigger the source generator to generate theEqualsCore
andGetHashCodeCOre
behind the sences
- Wrapper:
- Rules:
- Keep members immutable (get-only or init-only; fields
readonly
). - Equality attribute per member to define the equality behavior:
[IncludeInEquality]
,[IgnoreEquality]
,[SequenceEquality]
,[CustomEquality]
.
- Keep members immutable (get-only or init-only; fields
Tiny examples:
// Wrapper (best for single value)
public sealed class Email : ValueObject<Email, string>
{
public Email(string value) : base(value)
{
if (string.IsNullOrWhiteSpace(value)) throw new("email");
}
}
// Manual
// (In most cases, its recommended to use either wrapper (the above example) when you have a single value,
// Or use the [ValueObject] attribute (the eexample after this one) to trigger the source generator to implement the `EqualsCore` and `GetHashCodeCore` for you
public sealed class PersonName : ValueObject<PersonName>
{
public PersonName(string first, string second)
{
First = first;
Second = second;
}
public string First { get; }
public string Second { get; }
protected override bool EqualsCore(PersonName other) => First == other.First && Second == other.Second;
protected override int GetHashCodeCore() => HashCode.Combine(First, Second);
}
// Generator-driven
[ValueObject]
public sealed partial class Post : ValueObject<Post>
{
// The sequance equality here informs the source generator to compare the elements of the list one by one
// More about members equality attributes in the Guide section
[SequenceEquality] private readonly List<string> _comments;
public Post(string title, string body, List<string> comments)
{
Title = title;
Body = body;
_comments = comments;
}
[IncludeInEquality] public string Title { get; }
[IncludeInEquality] public string Body { get; }
// Here the analyzer knows that this is not an auto property or a field, and will not raise a warning for not implementing an equality attribute
public ReadOnlyList<string> Comments => _comments;
}
Enumerations
- What they are: Smart, type-safe alternatives to enums.
- Use when: You need named constants with behavior and lookup helpers.
- Rules: Make the class
partial
. Each instance has uniqueValue
andName
(The static code analyzer will raise and error when it finds a duplicate value or name). - Helpers:
GetAll()
,FromValue
,FromName
,TryFromValue
,TryFromName
. They are all generated automatically one you derive the class from theEnumeration
type and make itpartial
public sealed partial class OrderStatus : Enumeration
{
public static readonly OrderStatus Draft = new(0, "Draft");
public static readonly OrderStatus Submitted = new(1, "Submitted");
private OrderStatus(int value, string name) : base(value, name) { }
public bool CanSubmit() => this == Draft;
}
Optional generators: [GenerateJsonConverter(Behavior = UnknownValueBehavior.ReturnNull|ThrowException)]
, [GenerateEfValueConverter]
. More info in the Guides section.
Domain events and metadata
- What they are: Notifications raised by aggregates when something important happens.
- Rules: Events are records with
Id
andOccurredOn
. UseDomainEventWithMetadata
when you needUserId
,CorrelationId
,CausationId
.
var m = DomainEventMetadata.CreateWithUser("sara");
Event handlers and dispatcher
- Implement
IDomainEventHandler<TEvent>
; register via the non-genericIDomainEventHandler
so the dispatcher can find them. InMemoryDomainEventDispatcher
dispatches events to all matching handlers.
public sealed class OrderSubmittedHandler : IDomainEventHandler<OrderSubmitted>
{ public Task HandleAsync(OrderSubmitted e, CancellationToken ct = default) => Task.CompletedTask; }
// DI registration (Startup/Program)
services.AddSingleton<IDomainEventDispatcher, InMemoryDomainEventDispatcher>();
services.AddScoped<IDomainEventHandler, OrderSubmittedHandler>();
Specifications
- What they are: Reusable query logic with includes, sorting, paging, and composition.
- API:
Criteria
,Includes
,OrderBy
,OrderByDescending
,Take
,Skip
,IsSatisfiedBy
, and combinatorsAnd/Or/Not
.
public sealed class OrdersByStatus : Specification<Order>
{ public OrdersByStatus(OrderStatus s) : base(o => o.Status == s) { } }
Repositories
Repository<TEntity,TId>
wires domain event dispatch after your persistence hooks. Provide CRUD/Find/Count/Any and implement the core add/update/remove methods.- If you use
Repository<TEntity,TId>
, it will dispatchAggregateRoot.DomainEvents
and then callClearDomainEvents()
for you. - If you roll your own persistence, remember to dispatch domain events and clear them after a successful commit.
- If you use
Exceptions
- Its always advisable to use descriptive errors rather than normal exceptions for defining the domain-specific errors. This could be handled either by defining custom exceptions, or by using the result pattern.
- All derive from
DomainException
(withErrorCode
):EntityNotFoundException
,BusinessRuleViolationException
,DomainValidationException
,InvariantViolationException
,InvalidOperationDomainException
.
| Note: If you are using the result pattern to controll the flow of the domain-specific errors, then depending on how strictly you implement them, you might not use domain exceptions.
This is a good place to present my other 2 libraries which provide rich beneficial features in this area:
- FluentUnions: This is a rich library for the Result and Option patterns
- FluentEnforce: This includes built-in validations to throw exceptions when rules are not satisfied. It can be combined with this Exceptions of this library for doing specific validations and throw custom exceptions.
Services
IDomainService
: a small marker for domain services to provide behavior that don't belong to single aggregate.
Documentation
This root README is intentionally brief. For full and detailed documentation, see the docs on the repository:
- Index: docs index
- Getting started: docs/getting-started.md
- Guide: docs/guide.md
- API reference: docs/reference.md
- Examples: docs/examples.md
- Best practices: docs/best-practices.md
- Why DomainBase: docs/why-domainbase.md
- Contributing: docs/contributing.md
- FAQ: docs/faq.md
Links
- NuGet: DomainBase on NuGet
- Repository: github.com/ymjaber/domain-base
- License: MIT (LICENSE)
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net9.0 is compatible. 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. |
-
net9.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 |
---|---|---|
2.0.0 | 14 | 8/10/2025 |
2.0.0 – First public release.