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
<PackageReference Include="Sumapap.Ddd.Abstractions" Version="3.0.0" />
<PackageVersion Include="Sumapap.Ddd.Abstractions" Version="3.0.0" />
<PackageReference Include="Sumapap.Ddd.Abstractions" />
paket add Sumapap.Ddd.Abstractions --version 3.0.0
#r "nuget: Sumapap.Ddd.Abstractions, 3.0.0"
#:package Sumapap.Ddd.Abstractions@3.0.0
#addin nuget:?package=Sumapap.Ddd.Abstractions&version=3.0.0
#tool nuget:?package=Sumapap.Ddd.Abstractions&version=3.0.0
Sumapap.Ddd.Abstractions
💡 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
ConcurrentQueueensures 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
- Add the package to your domain project:
dotnet add package Sumapap.Ddd.Abstractions
- Define a domain event by implementing
IDomainEvent:
public record OrderPlacedEvent(Guid OrderId, DateTime PlacedAt) : IDomainEvent;
- 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));
}
}
- 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;
}
}
- 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
recordtypes 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.Dddor 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
recordtypes 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
DomainEntitymakes 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
- GitHub: muhammadirwanto-dev
- Project URL: https://github.com/muhammadirwanto-dev/sumapap
☕ 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 | Versions 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. |
-
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.