Reusable Domain/Application/Infrastructure building blocks — DDD, Clean Architecture, CQRS, and Modular Monolith patterns — for .NET.
🚧 Actively developed. Built to learn NuGet packaging and reduce copy-paste across my own projects — APIs may still change between versions.
dotnet add package aalmuqahwi.BuildingBlocks.Domain
dotnet add package aalmuqahwi.BuildingBlocks.Application
dotnet add package aalmuqahwi.BuildingBlocks.Infrastructure
Core DDD primitives. No external dependencies.
Result/Error— success/failure without exceptionsEntity<TId>/AggregateRoot<TId>— equality by ID, domain event trackingDomainEvent/IDomainEvent— immutable facts raised by aggregatesValueObject— base for immutable, structurally-equal types
CQRS and cross-cutting contracts. Depends only on BuildingBlocks.Domain.
ICommand/IQuery<TResult>— request markersICommandHandler<,>/IQueryHandler<,>— handler contractsIDispatcher— routes commands/queries to their handlersIPipelineBehavior<,>— cross-cutting middleware around handler execution (logging, validation, etc.)IUnitOfWork— persistence flush abstraction, technology-agnosticIDomainEventHandler<>/IDomainEventDispatcher— domain event handling contractsIIntegrationEvent/IIntegrationEventHandler<>/IIntegrationEventDispatcher— cross-boundary event contracts (module-to-module or service-to-service)IOutboxWriter— in-memory buffer for integration events raised during a unit of workOutboxMessage— persisted outbox record (Type,Payload,ProcessedOnUtc,Error,RetryCount)IOutboxMessageStore/IOutboxProcessor— outbox persistence and processing contracts, technology-agnostic
Concrete DI-based implementations. Depends on BuildingBlocks.Application.
Dispatcher— resolves and invokes command/query handlers via keyed DI, runs pipeline behaviorsDomainEventDispatcher— resolves and invokes all registered handlers for a given domain eventIntegrationEventDispatcher— resolves and invokes all registered handlers for a given integration eventOutboxWriter— in-memoryIOutboxWriterimplementationOutboxProcessor— reads pending outbox messages, dispatches them, and marks each processed or failed independently (one bad message never blocks the rest of the batch)AddBuildingBlocksInfrastructure(...)— one-line registration for dispatching (or callAddDispatcher(...)/AddDomainEventDispatcher(...)/AddOutboxProcessor()individually)
// Program.cs
builder.Services.AddBuildingBlocksInfrastructure(typeof(Program).Assembly);
// Somewhere in your app
public record CreateOrderCommand(string ProductId) : ICommand<Result<Guid>>;
public class CreateOrderHandler : ICommandHandler<CreateOrderCommand, Result<Guid>>
{
public Task<Result<Guid>> Handle(CreateOrderCommand command, CancellationToken ct)
=> Task.FromResult(Result.Success(Guid.NewGuid()));
}
// Dispatching
var result = await dispatcher.SendAsync(new CreateOrderCommand("prod-123"));Outbox / integration events — raise an event during a unit of work, flush it to storage when saving, then process it independently.
Raise and persist:
// Somewhere handling a command
_outboxWriter.Add(new OrderShipped(orderId));
// Inside your DbContext's SaveChangesAsync override (or equivalent)
foreach (var integrationEvent in _outboxWriter.Flush())
{
OutboxMessages.Add(new OutboxMessage
{
Id = Guid.NewGuid(),
Type = integrationEvent.GetType().AssemblyQualifiedName!,
Payload = JsonSerializer.Serialize(integrationEvent, integrationEvent.GetType()),
OccurredOnUtc = DateTime.UtcNow
});
}Register and process, triggered by whatever scheduler you prefer:
// Program.cs
builder.Services.AddOutboxProcessor();
builder.Services.AddScoped<IIntegrationEventHandler<OrderShipped>, SendShippingEmail>();
builder.Services.AddScoped<IOutboxMessageStore, EfOutboxMessageStore>(); // your own implementation
// Triggered by your own BackgroundService, Hangfire, Quartz, etc.
await outboxProcessor.ProcessPendingAsync(ct);Domainhas zero dependencies by design — safe to reference from anywhereIUnitOfWorkand persistence are intentionally technology-agnostic — bring your own EF Core/Dapper/etc. implementation, includingIOutboxMessageStore- Each package only depends on what it strictly needs —
ApplicationandInfrastructuredon't force unrelated dependencies onto consumers who only want a subset - Integration event handlers are registered manually, not auto-scanned like command/domain event handlers — unlike those, integration events may cross service boundaries, so implicit assembly scanning could pick up a handler that was never meant to run in a given service. Forgetting to register a handler causes it to silently never run — no error, no warning.
- Outbox delivery is at-least-once, not exactly-once. If the process crashes between dispatching and marking a message processed, it will be redelivered on the next run. Integration event handlers should be idempotent.
OutboxProcessorreports failures and computes the next retry count, but yourIOutboxMessageStoreowns retry policy.MarkFailedAsyncis called with the handler's actual error message and an incrementedRetryCount— it's up to your store implementation to decide when a message has retried too many times and stop returning it fromGetPendingAsync(or move it to a dead-letter state). Without this, a permanently unresolvable message will be retried forever.- No built-in scheduler for
IOutboxProcessor.ProcessPendingAsync()is a plain, injectable method — call it from aBackgroundService, a Hangfire recurring job, a Quartz job, or anything else. Trigger it from exactly one place; running two triggers concurrently against the same store can cause duplicate or racing delivery.