Sumapap.Ddd.Abstractions 3.0.0

dotnet add package Sumapap.Ddd.Abstractions --version 3.0.0
                    
NuGet\Install-Package Sumapap.Ddd.Abstractions -Version 3.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="Sumapap.Ddd.Abstractions" Version="3.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Sumapap.Ddd.Abstractions" Version="3.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Sumapap.Ddd.Abstractions" />
                    
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 Sumapap.Ddd.Abstractions --version 3.0.0
                    
#r "nuget: Sumapap.Ddd.Abstractions, 3.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 Sumapap.Ddd.Abstractions@3.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=Sumapap.Ddd.Abstractions&version=3.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Sumapap.Ddd.Abstractions&version=3.0.0
                    
Install as a Cake Tool

Sumapap.Ddd.Abstractions

NuGet Version NuGet Downloads License GitHub Issues GitHub Stars GitHub Forks Contributions Welcome

💡 Overview

Sumapap.Ddd.Abstractions provides the foundational building blocks for implementing Domain-Driven Design (DDD) patterns in your .NET applications. This package contains:

  • Domain Event Contracts — interfaces for defining and handling domain events
  • Domain Entity Base Class — thread-safe implementation for managing domain events within entities
  • Event Dispatcher Contract — abstraction for dispatching domain events to registered handlers

The package is designed to be lightweight, dependency-free, and focused exclusively on core DDD abstractions, making it suitable for use across domain, application, and infrastructure layers without introducing circular dependencies.

✨ Why use Sumapap.Ddd.Abstractions?

  • Zero Dependencies — No external packages required; only standard .NET types
  • Clean Architecture — Enables strict separation between domain logic and infrastructure concerns
  • Thread-Safe Event Management — Built-in ConcurrentQueue ensures safe event queuing in multi-threaded scenarios
  • Flexible Event Handling — Supports multiple handlers per event type through generic handler interface
  • Framework Agnostic — Can be used with any DI container or dispatcher implementation

🚀 Quick start

  1. Add the package to your domain project:
dotnet add package Sumapap.Ddd.Abstractions
  1. Define a domain event by implementing IDomainEvent:
public record OrderPlacedEvent(Guid OrderId, DateTime PlacedAt) : IDomainEvent;
  1. Create a domain entity that inherits from DomainEntity:
public class Order : DomainEntity
{
	public Guid Id { get; private set; }
	public OrderStatus Status { get; private set; }

	public void PlaceOrder()
	{
		Status = OrderStatus.Placed;

		// Raise domain event
		AddDomainEvent(new OrderPlacedEvent(Id, DateTime.UtcNow));
	}
}
  1. Implement an event handler:
public class OrderPlacedEventHandler : IDomainEventHandler<OrderPlacedEvent>
{
	public async Task HandleAsync(OrderPlacedEvent domainEvent, CancellationToken cancellationToken = default)
	{
		// Send confirmation email, update inventory, etc.
		await Task.CompletedTask;
	}
}
  1. Dispatch events after unit-of-work completes:
var order = new Order();
order.PlaceOrder();

// After saving to database
var events = order.ConsumeEvents(); // Gets and clears events
await dispatcher.DispatchAsync(events, cancellationToken);

🛠 Features and usage

IDomainEvent

A marker interface representing any domain event. Domain events capture meaningful business occurrences within your domain.

public interface IDomainEvent;

Best Practices:

  • Use record types for immutability
  • Include relevant contextual data (timestamps, entity IDs, user context)
  • Name events in past tense (e.g., OrderPlaced, PaymentProcessed)

Example:

public record ProductAddedToCartEvent(
	Guid CartId,
	Guid ProductId,
	int Quantity,
	DateTime AddedAt
) : IDomainEvent;

IDomainEventHandler<TEvent>

Generic interface for implementing event handlers. Each handler processes a specific event type.

public interface IDomainEventHandler<in TEvent> where TEvent : IDomainEvent
{
	Task HandleAsync(TEvent domainEvent, CancellationToken cancellationToken = default);
}

Features:

  • Contravariant (in TEvent) — allows handler covariance
  • Async-first — all handlers are asynchronous by design
  • Cancellation support — respects operation cancellation

Example with multiple handlers:

// Handler 1: Send notification
public class OrderPlacedNotificationHandler : IDomainEventHandler<OrderPlacedEvent>
{
	private readonly IEmailService _emailService;

	public async Task HandleAsync(OrderPlacedEvent domainEvent, CancellationToken cancellationToken)
	{
		await _emailService.SendOrderConfirmationAsync(domainEvent.OrderId, cancellationToken);
	}
}

// Handler 2: Update analytics
public class OrderPlacedAnalyticsHandler : IDomainEventHandler<OrderPlacedEvent>
{
	private readonly IAnalyticsService _analytics;

	public async Task HandleAsync(OrderPlacedEvent domainEvent, CancellationToken cancellationToken)
	{
		await _analytics.TrackEventAsync("OrderPlaced", domainEvent, cancellationToken);
	}
}

IDomainEventDispatcher

Contract for dispatching domain events to their registered handlers.

public interface IDomainEventDispatcher
{
	Task DispatchAsync(IEnumerable<IDomainEvent> domainEvents, CancellationToken cancellationToken = default);
}

Responsibilities:

  • Resolve all handlers for each event type
  • Invoke handlers in appropriate order
  • Handle exceptions or let them bubble (implementation-specific)

Note: This package provides only the abstraction. For a concrete implementation, use Sumapap.Ddd or implement your own dispatcher.

DomainEntity

Abstract base class for domain entities that need to raise domain events. Provides thread-safe event queueing.

public abstract class DomainEntity
{
	protected void AddDomainEvent(IDomainEvent domainEvent);
	public IReadOnlyList<IDomainEvent> ConsumeEvents();
	public IReadOnlyList<IDomainEvent> GetEvents();
	public void ClearEvents();
}

Methods:

Method Description
AddDomainEvent(event) Protected method to queue a domain event (call from within entity methods)
ConsumeEvents() Returns all queued events and clears the internal queue (use after persisting)
GetEvents() Returns all queued events without clearing the queue (read-only inspection)
ClearEvents() Clears all queued events without returning them (use for rollback scenarios)

Thread Safety:

  • Uses ConcurrentQueue<IDomainEvent> internally
  • Safe for concurrent access from multiple threads
  • No locking required

Example:

public class ShoppingCart : DomainEntity
{
	private readonly List<CartItem> _items = new();

	public void AddItem(Product product, int quantity)
	{
		ArgumentNullException.ThrowIfNull(product);

		if (quantity <= 0)
			throw new ArgumentException("Quantity must be positive", nameof(quantity));

		var item = new CartItem(product.Id, quantity, product.Price);
		_items.Add(item);

		// Raise domain event
		AddDomainEvent(new ProductAddedToCartEvent(
			CartId: Id,
			ProductId: product.Id,
			Quantity: quantity,
			AddedAt: DateTime.UtcNow
		));
	}

	public void Checkout()
	{
		if (!_items.Any())
			throw new InvalidOperationException("Cannot checkout an empty cart");

		// Business logic...

		AddDomainEvent(new CartCheckedOutEvent(Id, _items.Sum(i => i.Total)));
	}
}

// Usage in application layer
var cart = await _repository.GetByIdAsync(cartId);
cart.AddItem(product, quantity);

await _repository.SaveAsync(cart);

// Dispatch events AFTER successful save
var events = cart.ConsumeEvents();
await _dispatcher.DispatchAsync(events, cancellationToken);

⚠️ Notes & best practices

Event Dispatching Timing

  • Always dispatch events AFTER the unit-of-work/transaction completes successfully
  • Dispatching before save risks publishing events for uncommitted changes
  • Dispatching before commit risks publishing events that get rolled back
// ✅ CORRECT
await _unitOfWork.SaveChangesAsync();
var events = entity.ConsumeEvents();
await _dispatcher.DispatchAsync(events, cancellationToken);

// ❌ WRONG - events dispatched before save
var events = entity.ConsumeEvents();
await _dispatcher.DispatchAsync(events, cancellationToken);
await _unitOfWork.SaveChangesAsync(); // What if this fails?

Event Immutability

  • Use record types for events to ensure immutability
  • Avoid exposing mutable properties
  • Include all necessary context at creation time
// ✅ CORRECT - immutable record
public record OrderPlacedEvent(Guid OrderId, decimal Total, DateTime PlacedAt) : IDomainEvent;

// ❌ WRONG - mutable class
public class OrderPlacedEvent : IDomainEvent
{
	public Guid OrderId { get; set; }
	public decimal Total { get; set; }
}

Multiple Events

  • An entity can raise multiple events during a single operation
  • Events are queued in the order they are raised
  • All events are dispatched together after persistence
public void CompleteOrder()
{
	Status = OrderStatus.Completed;
	AddDomainEvent(new OrderCompletedEvent(Id, DateTime.UtcNow));

	if (IsFirstOrder)
		AddDomainEvent(new FirstOrderCompletedEvent(CustomerId, Id));

	if (Total > 1000)
		AddDomainEvent(new HighValueOrderCompletedEvent(Id, Total));
}

Handler Scope

  • Register handlers as Scoped services to allow access to scoped resources (DbContext, repositories)
  • Handlers should be side-effect operations (send emails, update read models, trigger workflows)
  • Keep handlers focused on a single responsibility

Error Handling

  • Decide on error handling strategy at the dispatcher level:
    • Fail-fast: Stop on first handler exception (propagate up)
    • Continue: Log exceptions but continue processing other handlers
    • Retry: Use resilience policies (e.g., Polly) for transient failures

Testing

  • DomainEntity makes testing easy — inspect raised events without needing a dispatcher:
[Fact]
public void PlaceOrder_RaisesOrderPlacedEvent()
{
	// Arrange
	var order = new Order();

	// Act
	order.PlaceOrder();

	// Assert
	var events = order.GetEvents();
	var orderPlacedEvent = events.OfType<OrderPlacedEvent>().Single();
	Assert.Equal(order.Id, orderPlacedEvent.OrderId);
}

Integration with Outbox Pattern

  • For reliable event delivery across process boundaries, consider using an outbox table:
// 1. Persist events to outbox table in same transaction as entity save
await _outboxRepository.AddAsync(events.Select(e => new OutboxMessage(e)));
await _unitOfWork.SaveChangesAsync();

// 2. Background worker reads outbox and publishes to message bus
// 3. Mark as processed after successful publish

⭐ License

This project is licensed under the MIT License. See the LICENSE file for details.

🚩 Contact

☕ Support

If you find this project helpful, consider supporting the developer:

<a href="https://www.buymeacoffee.com/muhammadirwanto" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>

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

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Sumapap.Ddd.Abstractions:

Package Downloads
Sumapap.Ddd

This package provides a simple, convention-based Domain Event Dispatcher that automatically discovers and invokes all registered `IDomainEventHandlerT` implementations for any given `IDomainEvent`. By leveraging .NET's built-in dependency injection, it ensures seamless integration with your application's service container, allowing for clean separation of concerns and adherence to DDD principles without coupling your domain layer to specific frameworks.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0 130 9/14/2026
2.1.0 181 5/25/2026
2.0.0 146 5/24/2026