Neominal.DddEventCore
1.0.1
dotnet add package Neominal.DddEventCore --version 1.0.1
NuGet\Install-Package Neominal.DddEventCore -Version 1.0.1
<PackageReference Include="Neominal.DddEventCore" Version="1.0.1" />
<PackageVersion Include="Neominal.DddEventCore" Version="1.0.1" />
<PackageReference Include="Neominal.DddEventCore" />
paket add Neominal.DddEventCore --version 1.0.1
#r "nuget: Neominal.DddEventCore, 1.0.1"
#:package Neominal.DddEventCore@1.0.1
#addin nuget:?package=Neominal.DddEventCore&version=1.0.1
#tool nuget:?package=Neominal.DddEventCore&version=1.0.1
DddEventCore
Lightweight DDD toolkit for .NET - Focus on domain logic, not infrastructure
DddEventCore is a lightweight, focused library for .NET developers building domain-driven applications. It provides essential DDD building blocks without the complexity of heavyweight frameworks.
β¨ Features
- π― Domain Events - Event-driven architecture with async handlers
- ποΈ Core DDD Building Blocks - Entity, Value Object, Aggregate Root, Domain Service
- π Async/Await Support - Modern async patterns throughout
- π Simple DI Integration - Works seamlessly with Microsoft.Extensions.DependencyInjection
- π¨ SOLID Principles - Clean, extensible architecture
- π¦ Minimal Dependencies - Zero magic, just clean abstractions
- π§ͺ Fully Tested - Comprehensive unit test coverage
- π Well Documented - Extensive documentation and examples
Perfect for teams adopting DDD without heavyweight frameworks.
π¦ Installation
dotnet add package Neominal.DddEventCore
Or via NuGet Package Manager:
Install-Package Neominal.DddEventCore
π Quick Start
1. Define Your Domain Event
using DddEventCore.Events;
public sealed record OrderPlacedEvent : IDomainEvent
{
public DateTime OccurredAt { get; }
public Guid EventId { get; }
public OrderId OrderId { get; }
public Guid CustomerId { get; }
public Money TotalAmount { get; }
public OrderPlacedEvent(OrderId orderId, Guid customerId, Money totalAmount)
{
OccurredAt = DateTime.UtcNow;
EventId = Guid.NewGuid();
OrderId = orderId;
CustomerId = customerId;
TotalAmount = totalAmount;
}
}
2. Create Your Aggregate Root
using DddEventCore.Domain;
public sealed class Order : AggregateRoot<OrderId>
{
public Guid CustomerId { get; private set; }
public Money TotalAmount { get; private set; }
public OrderStatus Status { get; private set; }
private Order(OrderId id, Guid customerId, Money totalAmount) : base(id)
{
CustomerId = customerId;
TotalAmount = totalAmount;
Status = OrderStatus.Pending;
}
public static Order Place(Guid customerId, Money totalAmount)
{
if (totalAmount.Amount <= 0)
throw new DomainException("Order total must be greater than zero");
var orderId = OrderId.CreateNew();
var order = new Order(orderId, customerId, totalAmount);
// Raise domain event
order.RaiseEvent(new OrderPlacedEvent(orderId, customerId, totalAmount));
return order;
}
public void Confirm()
{
if (Status != OrderStatus.Pending)
throw new DomainException("Cannot confirm order");
Status = OrderStatus.Confirmed;
RaiseEvent(new OrderConfirmedEvent(Id));
}
}
3. Create Event Handler
using DddEventCore.Events;
public class SendOrderConfirmationEmailHandler : IDomainEventHandler<OrderPlacedEvent>
{
private readonly IEmailService _emailService;
public SendOrderConfirmationEmailHandler(IEmailService emailService)
{
_emailService = emailService;
}
public async Task HandleAsync(OrderPlacedEvent domainEvent, CancellationToken cancellationToken = default)
{
await _emailService.SendOrderConfirmationAsync(
domainEvent.CustomerId,
domainEvent.OrderId,
cancellationToken);
}
}
4. Configure DI Container
using DddEventCore;
var builder = WebApplication.CreateBuilder(args);
// Add DddEventCore and scan for handlers
builder.Services.AddDddEventCore(typeof(Program).Assembly);
// Register your domain services
builder.Services.AddScoped<OrderPricingService>();
builder.Services.AddScoped<IEmailService, EmailService>();
var app = builder.Build();
5. Dispatch Events
public class OrderService
{
private readonly IDomainEventDispatcher _dispatcher;
private readonly IOrderRepository _repository;
public OrderService(IDomainEventDispatcher dispatcher, IOrderRepository repository)
{
_dispatcher = dispatcher;
_repository = repository;
}
public async Task PlaceOrderAsync(Guid customerId, Money amount)
{
// Create aggregate
var order = Order.Place(customerId, amount);
// Save to repository
await _repository.AddAsync(order);
// Dispatch domain events
await _dispatcher.DispatchAsync(order.GetDomainEvents());
// Clear events
order.ClearDomainEvents();
}
}
π Core Concepts
Entity
Entities are defined by their identity, not their attributes:
public class Customer : Entity<CustomerId>
{
public string Name { get; private set; }
public Email Email { get; private set; }
public Customer(CustomerId id, string name, Email email) : base(id)
{
Name = name;
Email = email;
}
}
Key Points:
- Identity-based equality
- Mutable state allowed
- Lifecycle matters
Value Object
Value Objects are defined by their attributes, not identity:
public sealed class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
if (amount < 0)
throw new DomainException("Money amount cannot be negative");
Amount = amount;
Currency = currency.ToUpperInvariant();
}
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new DomainException($"Cannot add different currencies");
return new Money(Amount + other.Amount, Currency);
}
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
}
Key Points:
- Structural equality
- Immutable
- Side-effect free operations
Aggregate Root
Aggregate Roots maintain consistency boundaries and collect domain events:
public class ShoppingCart : AggregateRoot<CartId>
{
private readonly List<CartItem> _items = new();
public IReadOnlyCollection<CartItem> Items => _items.AsReadOnly();
public void AddItem(ProductId productId, int quantity)
{
// Business logic
var existingItem = _items.FirstOrDefault(x => x.ProductId == productId);
if (existingItem != null)
{
existingItem.IncreaseQuantity(quantity);
}
else
{
_items.Add(new CartItem(productId, quantity));
}
// Raise event
RaiseEvent(new ItemAddedToCartEvent(Id, productId, quantity));
}
}
Key Points:
- Consistency boundary
- Collects domain events
- Controls access to entities within
Domain Service
Domain Services encapsulate business logic that doesn't belong to a single entity:
public class OrderPricingService : IDomainService
{
public Money CalculateTotalPrice(
IEnumerable<OrderItem> items,
Guid customerId,
Money shippingCost)
{
// Coordinates multiple aggregates:
// - OrderItems (Order aggregate)
// - Customer loyalty level (Customer aggregate)
// - Active promotions (Promotion aggregate)
var subtotal = items.Aggregate(
Money.Zero("USD"),
(sum, item) => sum.Add(item.Price.Multiply(item.Quantity)));
var discount = CalculateDiscount(customerId, subtotal);
return subtotal.Add(shippingCost).Subtract(discount);
}
private Money CalculateDiscount(Guid customerId, Money subtotal)
{
// Business logic involving multiple aggregates
return Money.Zero(subtotal.Currency);
}
}
When to Use Domain Services:
- β Coordinates multiple aggregates
- β Business logic requiring external dependencies (repositories)
- β Complex calculations not belonging to any single entity
- β Don't use for orchestration (that's Application Services)
- β Don't use for single-aggregate operations
Usage from Aggregate:
public static Order Place(
IEnumerable<OrderItem> items,
Guid customerId,
OrderPricingService pricingService)
{
var total = pricingService.CalculateTotalPrice(items, customerId, shipping);
var order = new Order(OrderId.CreateNew(), total);
order.RaiseEvent(new OrderPlacedEvent(...));
return order;
}
π Read the complete Domain Services Guide β
Domain Events
Domain Events represent something important that happened in the domain:
// Define event (use past tense)
public sealed record OrderPlacedEvent : IDomainEvent
{
public DateTime OccurredAt { get; }
public Guid EventId { get; }
public OrderId OrderId { get; }
public Guid CustomerId { get; }
public OrderPlacedEvent(OrderId orderId, Guid customerId)
{
OccurredAt = DateTime.UtcNow;
EventId = Guid.NewGuid();
OrderId = orderId;
CustomerId = customerId;
}
}
// Handle event (multiple handlers allowed)
public class SendEmailHandler : IDomainEventHandler<OrderPlacedEvent>
{
public async Task HandleAsync(OrderPlacedEvent @event, CancellationToken ct)
{
// Send confirmation email
}
}
public class UpdateInventoryHandler : IDomainEventHandler<OrderPlacedEvent>
{
public async Task HandleAsync(OrderPlacedEvent @event, CancellationToken ct)
{
// Update stock levels
}
}
Key Points:
- Named in past tense (something happened)
- Immutable (use records)
- Can have multiple handlers
- Dispatched after successful transaction
Domain Exception
Domain Exceptions represent business rule violations:
public class InsufficientStockException : DomainException
{
public InsufficientStockException(ProductId productId, int requested, int available)
: base($"Insufficient stock for product {productId}. Requested: {requested}, Available: {available}")
{
}
}
// Usage
if (stock.Quantity < requestedQuantity)
throw new InsufficientStockException(productId, requestedQuantity, stock.Quantity);
ποΈ Architecture
βββββββββββββββββββββββββββββββββββββββββββ
β Application Layer β
β ββββββββββββββββββββββββββββββββββββ β
β β Domain Event Handlers β β
β β Application Services β β
β ββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β Domain Layer β
β ββββββββββββββββββββββββββββββββββββ β
β β Aggregates (with Events) β β
β β Entities β β
β β Value Objects β β
β β Domain Services β β
β β Domain Events β β
β ββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β DddEventCore Library β
β ββββββββββββββββββββββββββββββββββββ β
β β IDomainEventDispatcher β β
β β AggregateRoot<T> β β
β β Entity<T> β β
β β ValueObject β β
β β IDomainService β β
β ββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββ
β‘ Best Practices
1. Event Naming
Event names should be in past tense (something has happened):
- β
OrderPlacedEvent - β
PaymentReceivedEvent - β
InventoryReservedEvent - β
PlaceOrderEvent - β
ReceivePaymentEvent
2. Immutable Events
Always use records or readonly properties:
// β
GOOD - Using record
public sealed record OrderPlacedEvent(OrderId OrderId, Guid CustomerId) : IDomainEvent
{
public DateTime OccurredAt { get; } = DateTime.UtcNow;
public Guid EventId { get; } = Guid.NewGuid();
}
// β
GOOD - Readonly properties
public class OrderPlacedEvent : DomainEventBase
{
public OrderId OrderId { get; }
public Guid CustomerId { get; }
public OrderPlacedEvent(OrderId orderId, Guid customerId)
{
OrderId = orderId;
CustomerId = customerId;
}
}
// β BAD - Mutable
public class OrderPlacedEvent : IDomainEvent
{
public OrderId OrderId { get; set; } // NO!
}
3. Rich Domain Events
Events should contain all information handlers need:
// β
GOOD - Contains all necessary information
public record ProductReservedEvent(
ProductId ProductId,
int Quantity,
OrderId OrderId,
DateTime ReservedUntil
);
// β BAD - Missing context
public record ProductReservedEvent(ProductId ProductId);
4. Respect Aggregate Boundaries
One aggregate should not directly modify another. Use events instead:
public class Order : AggregateRoot<OrderId>
{
public void Confirm()
{
Status = OrderStatus.Confirmed;
// β
GOOD - Raise event, let handler update inventory
RaiseEvent(new OrderConfirmedEvent(Id, Items));
}
}
// Handler updates different aggregate
public class ReserveInventoryHandler : IDomainEventHandler<OrderConfirmedEvent>
{
public async Task HandleAsync(OrderConfirmedEvent e, CancellationToken ct)
{
// Update Inventory aggregate
await _inventoryService.ReserveAsync(e.Items, ct);
}
}
5. Unit of Work Pattern
Dispatch events after successful SaveChanges:
public async Task SaveChangesAsync()
{
// Within transaction
await _dbContext.SaveChangesAsync();
// Only dispatch if transaction succeeded
var aggregates = _dbContext.ChangeTracker
.Entries<IAggregateRoot>()
.Select(e => e.Entity)
.ToList();
foreach (var aggregate in aggregates)
{
await _dispatcher.DispatchAsync(aggregate.GetDomainEvents());
aggregate.ClearDomainEvents();
}
}
π§ͺ Testing
Testing Aggregates
[Fact]
public void Order_Place_ShouldRaiseOrderPlacedEvent()
{
// Arrange
var customerId = Guid.NewGuid();
var amount = new Money(100m, "USD");
// Act
var order = Order.Place(customerId, amount);
var events = order.GetDomainEvents();
// Assert
events.Should().ContainSingle();
var @event = events.First().Should().BeOfType<OrderPlacedEvent>().Subject;
@event.CustomerId.Should().Be(customerId);
@event.TotalAmount.Should().Be(amount);
}
Testing Event Handlers
[Fact]
public async Task SendEmailHandler_ShouldSendEmail_WhenOrderPlaced()
{
// Arrange
var emailService = Substitute.For<IEmailService>();
var handler = new SendOrderConfirmationEmailHandler(emailService);
var @event = new OrderPlacedEvent(OrderId.CreateNew(), Guid.NewGuid(), new Money(100, "USD"));
// Act
await handler.HandleAsync(@event);
// Assert
await emailService.Received(1).SendOrderConfirmationAsync(
@event.CustomerId,
@event.OrderId,
Arg.Any<CancellationToken>());
}
Testing Event Dispatcher
[Fact]
public async Task Dispatcher_ShouldCallAllHandlers()
{
// Arrange
var services = new ServiceCollection();
var handler1 = new TestEventHandler1();
var handler2 = new TestEventHandler2();
services.AddSingleton<IDomainEventHandler<TestEvent>>(handler1);
services.AddSingleton<IDomainEventHandler<TestEvent>>(handler2);
services.AddSingleton<IDomainEventDispatcher, DomainEventDispatcher>();
var provider = services.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IDomainEventDispatcher>();
var @event = new TestEvent("test");
// Act
await dispatcher.DispatchAsync(@event);
// Assert
handler1.WasCalled.Should().BeTrue();
handler2.WasCalled.Should().BeTrue();
}
π Documentation
- Domain Services Guide - Comprehensive guide on when and how to use Domain Services
- Version 1.0 Summary - Detailed version summary and design decisions
- Examples - Sample applications demonstrating usage
πΊοΈ Roadmap
Version 2.0 (Planned)
- Integration Events - Communication between bounded contexts
- Outbox Pattern - Reliable event delivery with transactional guarantees
- Event Versioning - Schema evolution and backward compatibility
- Specification Pattern - Reusable business rule composition
Future Versions
- Saga Pattern support
- Event Sourcing capabilities
- Snapshot support
- Pipeline behaviors
π€ Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
π License
This project is licensed under the MIT License - see the LICENSE file for details.
π Acknowledgments
- Eric Evans - Domain-Driven Design
- Vaughn Vernon - Implementing Domain-Driven Design
- Microsoft - .NET Microservices Architecture
π Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Made with β€οΈ for the .NET DDD community
| 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 was computed. 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. |
-
net8.0
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 |
|---|---|---|
| 1.0.1 | 87 | 5/14/2026 |