Zwedze.Aetherweave.SharedKernel
0.2.5
dotnet add package Zwedze.Aetherweave.SharedKernel --version 0.2.5
NuGet\Install-Package Zwedze.Aetherweave.SharedKernel -Version 0.2.5
<PackageReference Include="Zwedze.Aetherweave.SharedKernel" Version="0.2.5" />
<PackageVersion Include="Zwedze.Aetherweave.SharedKernel" Version="0.2.5" />
<PackageReference Include="Zwedze.Aetherweave.SharedKernel" />
paket add Zwedze.Aetherweave.SharedKernel --version 0.2.5
#r "nuget: Zwedze.Aetherweave.SharedKernel, 0.2.5"
#:package Zwedze.Aetherweave.SharedKernel@0.2.5
#addin nuget:?package=Zwedze.Aetherweave.SharedKernel&version=0.2.5
#tool nuget:?package=Zwedze.Aetherweave.SharedKernel&version=0.2.5
Aetherweave.SharedKernel
Domain-Driven Design building blocks and primitives for building clean, maintainable domain models.
Features
- Aggregate Root - Base class for domain aggregates with built-in domain event support
- Type-Safe IDs - Generic value objects for entity identifiers
- Business Codes - Type-safe business identifiers and natural keys
- Domain Events - Event infrastructure for domain event patterns
- Business Exceptions - Base exception class for domain errors
Installation
dotnet add package Zwedze.Aetherweave.SharedKernel
Quick Start
1. Value Objects - Id<T> and Code<T>
Type-safe entity identifiers:
// Type-safe IDs prevent mixing different entity types
// Factory method (prefered)
var productId = Id<Product>.From(789);
// casting is allowed
var orderId = (Id<Order>)123L;
var customerId = (Id<Customer>)456L;
// orderId = customerId; // Compile error! ✅
Type-safe business codes:
// Natural keys and business identifiers
// Factory method (prefered)
var invoiceCode = Code<Invoice>.From("INV-2024-12345");
// casting is allowed
var orderCode = (Code<Order>)"ORD-2024-001";
var sku = (Code<Product>)"WIDGET-A1";
// orderCode = sku; // Compile error! ✅
Benefits:
- ✅ Compile-time type safety
- ✅ No mixing IDs or Codes from different entities
- ✅ Clear intent in code
- ✅ Validation built-in (no zero/negative IDs, no null/empty codes)
2. Aggregate Root
Define your domain aggregates:
public sealed class Order : AggregateRoot<Order>
{
public decimal Total { get; private set; }
public OrderStatus Status { get; private set; }
private readonly List<OrderItem> _items = [];
public Order(Id<Order> id, Code<Order> orderNumber)
: base(id, orderNumber)
{
Status = OrderStatus.Draft;
}
public void AddItem(Product product, int quantity)
{
var item = new OrderItem(product, quantity);
_items.Add(item);
Total += item.Price;
// Raise domain event
RaiseDomainEvent(new OrderItemAddedEvent(Id, product.Id, quantity));
}
public void Submit()
{
if (!_items.Any())
throw new OrderEmptyException();
Status = OrderStatus.Submitted;
// Raise domain event
RaiseDomainEvent(new OrderSubmittedEvent(Id, Total));
}
}
Aggregate equality:
var order1 = new Order(Id<Order>.From(1), Code<Order>.From("ORD-001"));
var order2 = new Order(Id<Order>.From(1), Code<Order>.From("ORD-002"));
// Equality based on Id
order1 == order2 // true - same entity (same Id)
order1.Equals(order2) // true
3. Domain Events
Define domain events:
public record OrderSubmittedEvent(Id<Order> OrderId, decimal Total) : DomainEvent;
public record OrderItemAddedEvent(
Id<Order> OrderId,
Id<Product> ProductId,
int Quantity) : DomainEvent;
public record OrderCancelledEvent(Id<Order> OrderId, string Reason) : DomainEvent;
Recommended to expose the minimum required data and avoid complex payloads.
Raise events from aggregates:
public sealed class Order : AggregateRoot<Order>
{
public void Cancel(string reason)
{
Status = OrderStatus.Cancelled;
// Event automatically tracked
RaiseDomainEvent(new OrderCancelledEvent(Id, reason));
}
}
Dispatch events after persistence:
public sealed class OrderService(
IOrderRepository orderRepository,
IDomainEventDispatcher eventDispatcher)
{
public async Task SubmitOrder(Id<Order> orderId, CancellationToken ct)
{
var order = await orderRepository.GetByIdAsync(orderId, ct);
order.Submit();
await orderRepository.SaveAsync(order, ct);
// Pop and dispatch events
await eventDispatcher.DispatchAsync(order, ct);
}
}
4. Business Exceptions
Define domain exceptions:
Mandatory to provide a business code and a message. But it is recommended to include additional context for the developer.
public sealed class InsufficientStockException(int requested, int available)
: BusinessException(
$"Insufficient stock. Requested: {requested}, Available: {available}",
"STOCK_001")
{
public int Requested { get; } = requested;
public int Available { get; } = available;
}
public sealed class OrderNotFoundException(Id<Order> orderId)
: BusinessException(
$"Order {orderId} not found",
"ORDER_001")
{
public Id<Order> OrderId { get; } = orderId;
}
public sealed class InvalidOrderStateException(OrderStatus currentState, OrderStatus requiredState)
: BusinessException(
$"Order must be in {requiredState} state, but is in {currentState}",
"ORDER_002")
{
public OrderStatus CurrentState { get; } = currentState;
public OrderStatus RequiredState { get; } = requiredState;
}
Use in domain logic:
public sealed class Product : AggregateRoot<Product>
{
public int StockQuantity { get; private set; }
public void ReserveStock(int quantity)
{
if (quantity > StockQuantity)
{
throw new InsufficientStockException(quantity, StockQuantity);
}
StockQuantity -= quantity;
RaiseDomainEvent(new StockReservedEvent(Id, quantity));
}
}
Advanced Usage
Custom Aggregate Root
Without Code (Id only):
If you don't need business codes, you can create a simpler base class:
public abstract class AggregateRootWithIdOnly<TEntity>(Id<TEntity> id) : IHasDomainEvents
{
private readonly List<IDomainEvent> _events = [];
public Id<TEntity> Id { get; } = id;
public ImmutableArray<IDomainEvent> PopDomainEvents()
{
var events = _events.ToImmutableArray();
_events.Clear();
return events;
}
protected void RaiseDomainEvent(IDomainEvent domainEvent)
{
_events.Add(domainEvent);
}
// Equality based on Id...
}
Value Object Conversions
To primitives:
var orderId = Id<Order>.From(123);
long idValue = orderId; // Implicit cast to long
var orderCode = Code<Order>.From("ORD-001");
string codeValue = orderCode; // Implicit cast to string
From primitives:
// Factory method (recommended)
var id2 = Id<Order>.From(123);
var code2 = Code<Order>.From("ORD-001");
// Explicit cast
var id = (Id<Order>)123L;
var code = (Code<Order>)"ORD-001";
Domain Event Metadata
All domain events include:
public interface IDomainEvent
{
Guid Id { get; } // Unique event ID
DateTimeOffset OccurredOn { get; } // When event occurred (UTC)
}
Access in handlers (implemented in the Application layer):
public sealed class OrderSubmittedHandler : IDomainEventHandler<OrderSubmittedEvent>
{
public async Task HandleAsync(OrderSubmittedEvent @event, CancellationToken ct)
{
// Event metadata available
var eventId = @event.Id;
var occurredAt = @event.OccurredOn;
// Event-specific data
var orderId = @event.OrderId;
var total = @event.Total;
// Handle event...
}
}
Architecture Patterns
Typical Aggregate Structure
public sealed class Order : AggregateRoot<Order>
{
// 1. Private fields for encapsulation
private readonly List<OrderItem> _items = [];
// 2. Public properties (read-only from outside)
public decimal Total { get; private set; }
public OrderStatus Status { get; private set; }
public Id<Customer> CustomerId { get; private set; }
// 3. Constructor (factory method)
public Order(Id<Order> id, Code<Order> orderNumber, Id<Customer> customerId)
: base(id, orderNumber)
{
CustomerId = customerId;
Status = OrderStatus.Draft;
}
// 4. Business methods that enforce invariants
public void AddItem(Product product, int quantity)
{
if (Status != OrderStatus.Draft)
throw new InvalidOrderStateException(Status, OrderStatus.Draft);
var item = new OrderItem(product, quantity);
_items.Add(item);
Total += item.Price;
RaiseDomainEvent(new OrderItemAddedEvent(Id, product.Id, quantity));
}
public void Submit()
{
if (!_items.Any())
throw new OrderEmptyException();
if (Status != OrderStatus.Draft)
throw new InvalidOrderStateException(Status, OrderStatus.Draft);
Status = OrderStatus.Submitted;
RaiseDomainEvent(new OrderSubmittedEvent(Id, Total));
}
}
Repository Pattern
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Id<Order> id, CancellationToken ct);
Task<Order?> GetByCodeAsync(Code<Order> orderNumber, CancellationToken ct);
Task AddAsync(Order order, CancellationToken ct);
Task SaveAsync(Order order, CancellationToken ct);
}
Best Practices
✅ DO
Use Id<T> and Code<T> everywhere:
// Good public void AssignOrder(Id<Order> orderId, Id<Driver> driverId) { } // Bad public void AssignOrder(long orderId, long driverId) { } // Easy to mix up!Raise domain events for important state changes:
public void ApproveOrder() { Status = OrderStatus.Approved; RaiseDomainEvent(new OrderApprovedEvent(Id)); // ✅ }Keep aggregates consistent:
public void Cancel(string reason) { // Validate if (Status == OrderStatus.Delivered) throw new OrderAlreadyDeliveredException(); // Update state Status = OrderStatus.Cancelled; CancellationReason = reason; // Raise event RaiseDomainEvent(new OrderCancelledEvent(Id, reason)); }Use business exceptions for domain errors:
if (quantity > StockQuantity) throw new InsufficientStockException(quantity, StockQuantity);
❌ DON'T
Don't bypass encapsulation:
// Bad order.Status = OrderStatus.Cancelled; // Public setter // Good order.Cancel(reason); // Business methodDon't raise events for every property change:
// Bad - too granular RaiseDomainEvent(new OrderTotalChangedEvent(oldTotal, newTotal)); // Good - meaningful business events RaiseDomainEvent(new OrderSubmittedEvent(Id, Total));Don't use primitive types for IDs:
// Bad public void Ship(long orderId) { } // Good public void Ship(Id<Order> orderId) { }Don't make aggregates too large:
// Bad - Order managing inventory, shipping, billing public sealed class Order : AggregateRoot<Order> { public void ManageInventory() { } public void ProcessShipping() { } public void GenerateInvoice() { } } // Good - Focused aggregate public sealed class Order : AggregateRoot<Order> { public void Submit() { } public void Cancel(string reason) { } }
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Zwedze.Aetherweave.SharedKernel:
| Package | Downloads |
|---|---|
|
Zwedze.Aetherweave.IdentityGenerators
Distributed Snowflake-based identity generation for Aetherweave, providing type-safe, sortable, and high-performance identifiers for multi-instance environments. |
|
|
Zwedze.Aetherweave.Application
Clean architecture application layer for Aetherweave, providing CQRS handlers, a fluent domain event dispatcher, and standardized Result patterns. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.2.5 | 107 | 9/3/2026 |
| 0.2.5-pre.2 | 70 | 9/3/2026 |
| 0.2.5-pre.1 | 76 | 8/13/2026 |
| 0.2.5-extend-db-registratio... | 63 | 8/31/2026 |
| 0.2.4 | 122 | 8/13/2026 |
| 0.2.4-pre.1 | 71 | 8/13/2026 |
| 0.2.3 | 117 | 8/6/2026 |
| 0.2.3-pre.1 | 78 | 8/6/2026 |
| 0.2.2 | 129 | 8/5/2026 |
| 0.2.2-pre.1 | 74 | 7/30/2026 |
| 0.2.1 | 131 | 7/29/2026 |
| 0.2.1-pre.1 | 74 | 7/29/2026 |
| 0.2.0 | 129 | 7/25/2026 |
| 0.1.2-pre.3 | 75 | 7/25/2026 |
| 0.1.2-pre.1 | 71 | 7/24/2026 |
| 0.1.2-http-auth.1 | 72 | 7/23/2026 |
| 0.1.1 | 138 | 6/16/2026 |
| 0.1.0-alpha.12 | 79 | 6/16/2026 |
| 0.0.1-pre.14 | 85 | 6/16/2026 |
| 0.0.1-pre.13 | 76 | 6/16/2026 |