Skip to content

cwouyang/ezDDD.NET

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

130 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ezDDD.NET

Tactical Domain-Driven Design patterns library for .NET 8+

Based on Java ezddd 6.0.1

A modern tactical DDD library for .NET with event sourcing, state sourcing, and CQRS patterns. This is a faithful .NET port of the Java ezddd 6.0.1 library (GitLab commit: 3aac0f5) with ~99% semantic parity and .NET-specific improvements.

Build and Test NuGet .NET License Status


Status

ezDDD.NET follows Semantic Versioning and is published on NuGet β€” the current version is shown by the NuGet badge above, and you can install it from the Quick Start. Changes between releases are tracked in CHANGELOG.md.


Table of Contents


Quick Start

Installation

dotnet add package ezDDD.Core

ezDDD.Core is the all-in-one aggregator; individual modules (ezDDD.Common, ezDDD.Entity, ezDDD.UseCase, ezDDD.Cqrs) can also be installed separately β€” see Modules.

Basic Usage: Event-Sourced Aggregate

using EzDdd.Entity;

// Aggregate ID (ToString() drives the stream name: "account-ACC-001")
public sealed record AccountId(string Value) : IValueObject
{
    public override string ToString() => Value;
}

// Domain events are immutable records
public sealed record AccountCreated(
    Guid Id, DateTimeOffset OccurredOn, AccountId Source, string Owner, decimal InitialBalance
) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent
{
    string IDomainEvent.Source => Source.Value;
    IReadOnlyDictionary<string, string> IDomainEvent.Metadata => new Dictionary<string, string>();
}

public sealed record MoneyDeposited(
    Guid Id, DateTimeOffset OccurredOn, AccountId Source, decimal Amount
) : IInternalDomainEvent
{
    string IDomainEvent.Source => Source.Value;
    IReadOnlyDictionary<string, string> IDomainEvent.Metadata => new Dictionary<string, string>();
}

// Event-sourced aggregate
public sealed class BankAccount : EsAggregateRoot<AccountId, IInternalDomainEvent>
{
    public BankAccount(AccountId id, string owner, decimal initialBalance)
    {
        Id = id;
        Apply(new AccountCreated(Guid.NewGuid(), DateTimeOffset.UtcNow, id, owner, initialBalance)); // R1
    }

    public BankAccount(IEnumerable<IInternalDomainEvent> events) : base(events) { } // event replay

    public string Owner { get; private set; } = string.Empty;
    public decimal Balance { get; private set; }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new InvalidOperationException("Deposit amount must be positive");
        Apply(new MoneyDeposited(Guid.NewGuid(), DateTimeOffset.UtcNow, Id, amount)); // R2
    }

    protected override void _When(IInternalDomainEvent @event)
    {
        switch (@event)
        {
            case AccountCreated created:
                Id = created.Source;
                Owner = created.Owner;
                Balance = created.InitialBalance;
                break;
            case MoneyDeposited deposited:
                Balance += deposited.Amount;
                break;
        }
    }

    protected override void _EnsureInvariant()
    {
        if (Balance < 0)
            throw new InvalidOperationException("Balance cannot be negative");
    }

    public override string GetCategory() => "account";
}

This is a trimmed version of the compile-verified BankAccount example; the full version (with a Money value object, withdraw/close, and the EsRepository round-trip) is in USAGE_EXAMPLES.md.


Features

Core Capabilities

  • βœ… Tactical DDD building blocks: IEntity<TId>, IValueObject, AggregateRoot<TId, TEvent>, domain events with Metadata for idempotency and distributed tracing
  • βœ… Event sourcing: EsAggregateRoot<TId, TEvent> enforcing R1/R2/R3 invariant rules via template method, with event replay and {category}-{id} stream naming
  • βœ… State sourcing: OutboxRepository persists aggregate state + events atomically (Transactional Outbox)
  • βœ… Repository bridge pattern: IRepository (domain abstraction) ↔ IRepositoryPeer (persistence SPI; the transaction boundary)
  • βœ… CQRS: ICommand / IQuery / IInquiry / IProjection with the CqrsOutput<T> fluent output API
  • βœ… Event reaction: IReactor<TInput> hierarchy β€” IProjector<TInput> maintains read models, INotifier<TInput> converts internal events to external (integration) events
  • βœ… External publishing: IExternalDomainEventPublisher<TEvent> out-port; repositories never publish β€” a separate Relay does (see examples/EventInfrastructure/)
  • βœ… System reconciliation: IReconciler<TContext, TReport> for maintenance jobs (cleanup, consistency checks)

Design Philosophy

  • πŸš€ Async/await throughout: All I/O operations return Task<T>, never blocking
  • 🎯 Clean Architecture: Unidirectional dependencies (Common β†’ Entity β†’ UseCase β†’ Cqrs), ports & adapters
  • πŸ“¦ Zero external dependencies: Only .NET BCL + uContract.NET (ecosystem dependency)
  • πŸ”’ Thread-safe: Concurrent collections, Lazy<T>, and snapshot patterns
  • πŸ’Ž Strongly typed: Generic variance (in TInput, out TOutput) and nullable reference types
  • 🧬 Modern C# idioms: Records for events/value objects, pattern matching for event handlers
  • πŸ§ͺ Highly tested: 543 tests passing, >90% coverage across all modules
  • 🀝 Semantic parity: ~99% parity with Java ezddd 6.0.1, upstream tracked per release

Modules

Five NuGet packages with a unidirectional dependency chain (Common β†’ Entity β†’ UseCase β†’ Cqrs β†’ Core):

NuGet Package Purpose Depends on
ezDDD.Common Foundation utilities (BiMap, JsonUtil, Converter) β€”
ezDDD.Entity Core DDD patterns (entities, value objects, aggregates, domain events) Common, uContract
ezDDD.UseCase Use cases, repositories (event/state sourcing), event infrastructure Entity
ezDDD.Cqrs CQRS patterns (commands, queries, projections, CqrsOutput) UseCase
ezDDD.Core ⭐ All-in-one aggregator (no code of its own) All of the above

Note: Package IDs use the ezDDD.* prefix; namespaces use EzDdd.* (e.g. using EzDdd.Entity;).


API Overview

πŸ“– Complete Documentation: API_REFERENCE.md β€” signatures, parameters, exceptions, and examples for every public API

Module Key Types
Common BiMap<TKey, TValue>, JsonUtil, Converter<TSource, TTarget>
Entity IEntity<TId>, IValueObject, IDomainEvent, IInternalDomainEvent, AggregateRoot<TId, TEvent>, EsAggregateRoot<TId, TEvent>, DomainEventTypeMapper
UseCase IUseCase<TInput, TOutput>, IReactor<TInput>, IReconciler<TContext, TReport>, IRepository<TAggregate, TId, TEvent>, IRepositoryPeer<TData, TId>, EsRepository<TAggregate, TId>, OutboxRepository<TAggregate, TData, TId>, IExternalDomainEventPublisher<TEvent>, ExitCode
Cqrs ICommand<TInput, TOutput>, IQuery<TInput, TOutput>, IInquiry<TInput, TOutput>, IProjection<TInput, TOutput>, IProjector<TInput>, INotifier<TInput>, IArchive<TData, TId>, CqrsOutput<T>

Examples

πŸ“š More Examples: USAGE_EXAMPLES.md (30+ compile-verified scenarios)

  • Basic Patterns: Aggregates, value objects, domain events β†’ USAGE_EXAMPLES.md
  • Event Sourcing: BankAccount aggregate, replay, R1/R2/R3 rules, EsRepository β†’ USAGE_EXAMPLES.md
  • State Sourcing: Transactional Outbox, OutboxRepository, OutboxMapper β†’ USAGE_EXAMPLES.md
  • CQRS: Commands, queries, projections, CqrsOutput fluent API β†’ USAGE_EXAMPLES.md
  • System Reconciliation: Cleanup reconcilers, NullContext, scheduling β†’ USAGE_EXAMPLES.md
  • Real-World Scenarios: Banking, e-commerce, inventory, order management β†’ USAGE_EXAMPLES.md
  • Relay Pattern: Reference implementation of EventStoreRelay (event store β†’ publisher) β†’ examples/EventInfrastructure/

Requirements

  • .NET 8.0 or later (C# 12, nullable reference types enabled)
  • uContract 1.0.0+ β€” Design by Contract support (same ecosystem as Java ezddd's uContract); used by EsAggregateRoot invariant checking
  • No other dependencies β€” production code uses only .NET built-in APIs (System.Text.Json, System.Reflection, System.Collections.Concurrent)

Differences from Java Version

ezDDD.NET maintains ~99% semantic parity with Java ezddd 6.0.1 β€” core patterns (aggregates, R1/R2/R3 rules, repository bridge, Transactional Outbox, CQRS separation, reactor hierarchy) behave identically β€” while adopting .NET platform idioms.

Syntax and Platform Differences

Aspect Java ezddd C# ezDDD.NET
Async Synchronous execute(input) ExecuteAsync(input) returns Task<T>, non-blocking
Method naming camelCase() PascalCase() + Async suffix for async methods
Protected overrides when(), ensureInvariant() _When(), _EnsureInvariant() (underscore prefix)
Null safety Optional<T>, @Nullable Nullable reference types (T?), compiler-enforced
Generics <ID, E> <TId, TEvent> with variance (in / out)
Event handling instanceof chains Pattern matching with switch
Immutability final fields, getters record types with init properties
Serialization Jackson System.Text.Json (built-in)

API Mapping Highlights

Java ezddd C# ezDDD.NET
Optional<T> findById(ID) Task<T?> FindByIdAsync(TId)
addDomainEvent(E) / getDomainEvents() _AddDomainEvent(TEvent) / GetDomainEvents()
CqrsOutput.create().succeed() CqrsOutput<T>.Create().Succeed()
MessageProducer (moved to ezddd-gateway in 6.0.0) Excluded from core, matching Java; a .NET Gateway package is deferred post-1.0

Example Comparison

Java:

public class BankAccount extends EsAggregateRoot<AccountId, InternalDomainEvent> {
    private Money balance;

    public void deposit(Money amount) {
        var event = new MoneyDeposited(UUID.randomUUID(), Instant.now(), id, amount);
        apply(event);
    }

    @Override
    protected void when(InternalDomainEvent event) {
        if (event instanceof MoneyDeposited deposited) {
            this.balance = balance.add(deposited.amount());
        }
    }
}

C#:

public sealed class BankAccount : EsAggregateRoot<AccountId, IInternalDomainEvent>
{
    private Money _balance = new(0);

    public void Deposit(Money amount)
    {
        var @event = new MoneyDeposited(Guid.NewGuid(), DateTimeOffset.UtcNow, Id, amount);
        Apply(@event);
    }

    protected override void _When(IInternalDomainEvent @event)
    {
        switch (@event)
        {
            case MoneyDeposited deposited:
                _balance = _balance.Add(deposited.Amount);
                break;
        }
    }
}

Migrating from Java

See MIGRATION_GUIDE.md for side-by-side Java/C# comparisons of every pattern, syntax mapping tables, and common gotchas.


Documentation

User Documentation

  • πŸ“– API_REFERENCE.md - Complete API reference
    • Every public type and method with signatures, exceptions, and examples
    • Verified against the shipped public API baseline
  • πŸ“š USAGE_EXAMPLES.md - Real-world examples
    • 30+ compile-verified scenarios (banking, e-commerce, inventory, order management)
    • Event sourcing, state sourcing, CQRS, and reconciliation walkthroughs
  • πŸ”„ MIGRATION_GUIDE.md - Java β†’ .NET migration guide
  • πŸ“ CHANGELOG.md - Release history (Keep a Changelog / SemVer)

Developer Documentation

  • πŸ‘¨β€πŸ’» AGENTS.md - Development standards and workflow (TDD, Tidy First, build/test commands)
  • πŸ“‹ docs/adr/ - Architecture Decision Records documenting design rationale
  • πŸ—ΊοΈ ROADMAP.md - Current status and post-1.0 considerations

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for development guidelines.

Before contributing:

  1. Read the Architecture Decision Records to understand design rationale
  2. Follow the standards in AGENTS.md β€” tests first (TDD), >90% coverage

License

MIT License β€” Copyright (c) 2025-2026 ezDDD.NET Contributors. See LICENSE for details.

This project is a derivative work of the Java ezddd library by Teddy Chen and contributors, which is licensed under the Apache License 2.0. See THIRD-PARTY-NOTICES.txt for the required attribution and license text.


References

Original Java Version (6.0.1)

This .NET port is based on Java ezddd 6.0.1 (GitLab commit: 3aac0f5; synchronized 2.1.0 β†’ 4.1.0 β†’ 6.0.1 before first publication)

Theory


About

Tactical DDD patterns library for .NET 8 - event sourcing, state sourcing, and CQRS (port of Java ezddd 6.0.1)

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages