DomainBase 2.0.0

dotnet add package DomainBase --version 2.0.0
                    
NuGet\Install-Package DomainBase -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DomainBase" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DomainBase" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="DomainBase" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DomainBase --version 2.0.0
                    
#r "nuget: DomainBase, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DomainBase@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DomainBase&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=DomainBase&version=2.0.0
                    
Install as a Cake Tool

DomainBase

DomainBase

NuGet Downloads License: MIT GitHub

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.
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 own EqualsCore/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 the EqualsCore and GetHashCodeCOre behind the sences
  • Rules:
    • Keep members immutable (get-only or init-only; fields readonly).
    • Equality attribute per member to define the equality behavior: [IncludeInEquality], [IgnoreEquality], [SequenceEquality], [CustomEquality].

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 unique Value and Name (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 the Enumeration type and make it partial
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 and OccurredOn. Use DomainEventWithMetadata when you need UserId, CorrelationId, CausationId.
var m = DomainEventMetadata.CreateWithUser("sara");

Event handlers and dispatcher

  • Implement IDomainEventHandler<TEvent>; register via the non-generic IDomainEventHandler 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 combinators And/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 dispatch AggregateRoot.DomainEvents and then call ClearDomainEvents() for you.
    • If you roll your own persistence, remember to dispatch domain events and clear them after a successful commit.

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 (with ErrorCode): 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:

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.