MartinDrozdik.DDD
0.11.0
dotnet add package MartinDrozdik.DDD --version 0.11.0
NuGet\Install-Package MartinDrozdik.DDD -Version 0.11.0
<PackageReference Include="MartinDrozdik.DDD" Version="0.11.0" />
<PackageVersion Include="MartinDrozdik.DDD" Version="0.11.0" />
<PackageReference Include="MartinDrozdik.DDD" />
paket add MartinDrozdik.DDD --version 0.11.0
#r "nuget: MartinDrozdik.DDD, 0.11.0"
#:package MartinDrozdik.DDD@0.11.0
#addin nuget:?package=MartinDrozdik.DDD&version=0.11.0
#tool nuget:?package=MartinDrozdik.DDD&version=0.11.0
Flexible Domain-Driven Design (DDD) library for .NET
A pragmatic .NET library for Domain-Driven Design that doesn't force you into abstract nonsense (that much). Check the demo.
Installation
dotnet add package MartinDrozdik.DDD
duh...
Also check this repos' DDD Claude Code plugin for better AI code generation.
Philosophy
- Pragmatic over purist - Use what makes sense, ignore what doesn't
- Composition over inheritance - Depth is the enemy of maintainability
- Explicit over implicit - Code should be obvious, not something you need to explain in a meeting
- Fewer layers, sharper boundaries – Layers exist to solve problems, not to satisfy diagrams or blog posts
- Type safety - Compiler errors > Runtime errors (reflection is for edge cases, not architecture)
- Fail fast - Built with validation and error handling in mind
Use the parts that help. Ignore the rest. Good luck.
Demo Project
The demo project shows recommended patterns. It's not gospel, but it works. Check out:
- Models/ - Aggregates, Entities, Value Objects, Enumerations
- Requests/ - Commands and Queries with handlers
- Context/ - EF Core configuration with identity converters
Templates (keeping it simple, stupid)
Basic interfaces that define DDD building blocks. These are interfaces, not abstract classes, because your ORM probably needs that flexibility anyway. Also, no base classes with 100 methods you don't need. Just the essentials.
Check out the demo for more examples with validation and other goodies:
- Person entity with validation and strongly typed ID
- Invoice aggregate with validation and strongly typed ID
- Value Object with validation for an Invoice Number
- In-Memory state enumeration
Value Objects
Compared by value, not identity:
public class InvoiceNumber : ValueObject
{
private InvoiceNumber(int year, int order)
{
Year = year;
Order = order;
}
public int Year { get; }
public int Order { get; }
public static InvoiceNumber Create(int year, int order)
{
var result = new InvoiceNumber(year, order);
return result;
}
// Takes care of equality and hashing based on the properties you yield here:
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Year;
yield return Order;
}
}
Entities
Compared by identity, not value. Just implement the marker interface and you're good to go:
public class Person : IDomainEntity<PersonId> // or IAggregateRoot<PersonId>
{
private Person(PersonId id, string fullName)
{
Id = id;
FullName = fullName;
}
public PersonId Id { get; }
public string FullName { get; }
public static Person Create(string fullName)
{
// Validate...
return new Person(new PersonId(Guid.CreateVersion7()), fullName);
}
}
Aggregates
Your consistency boundaries (aka "units of work that make sense").
Same implementation as Entity, just a different marker interface IAggregateRoot<Guid>.
Identities
Strongly-Typed IDs (no more Guid soup).
Stop passing around naked Guids and ints like it's 2010. Type-safe identities with conversion support:
public class PersonId(Guid key) : GuidIdentity<PersonId>(key);
// Usage:
interface ISomeService
{
Person Get(PersonId id);
}
var invoice = someService.Get(id); // Type-safe!
Built-in primitives:
GuidIdentity<T>- For when you want GUIDs (useGuid.CreateVersion7()like a civilized person)IntIdentity<T>- For when you're stuck with legacy databasesStringIdentity<T>- For when external systems hate you
EF Core support:
builder.Property(e => e.Id)
.HasIdentityConvertor(new IdentityConverter<InvoiceId, Guid>());
Enumerations
Because enum is fine until you need behavior and more properties, then you're screwed.
public class InvoiceState(EnumerationName name /*and more properties as you like*/)
: Enumeration(name)
{
public static InvoiceState Draft => new(new EnumerationName("Draft"));
public static InvoiceState Issued => new(new EnumerationName("Issued"));
public static InvoiceState Paid => new(new EnumerationName("Paid"));
// Add methods, validations, whatever you need
public bool CanBeModified() => this == Draft;
}
// Usage
var state = InvoiceState.Draft;
if (state.CanBeModified()) { /* ... */ }
Serializes to strings/properties in your DB/JSON. Extends like a class. It's beautiful 😍.
Enumeration is a ValueObject, so equality comes for free – only the name is compared case-sensitively.
Exposing enumerations through an API
Your enumeration is a domain type. You don't want it on the public API surface, so you declare a plain enum for the contract – and then you're stuck writing a mapping method and keeping it in sync by hand. Don't.
public enum InvoiceStateDto
{
Draft,
Issued,
Paid,
}
// domain -> API
State = invoice.State.ToStructEnum<InvoiceStateDto>(),
// API -> domain
var state = InvoiceState.FromStructEnum(dto.State);
Members are matched by name, case-sensitively. Both directions have ...Optional sibling that maps null to null: ToStructEnumOptional and FromStructEnumOptional.
When the names must differ, tell it directly:
public enum InvoiceStateDto
{
Draft,
Issued,
[EnumerationName("Paid")]
Settled,
}
Check the whole mapping up front, at startup or from a test:
EnumerationStructMapping.ThrowIfIncomplete<InvoiceState, InvoiceStateDto>();
Also works with FluentValidation:
RuleFor(x => x.State).MustMapToEnumeration(EnumerationMap.To<InvoiceState>());
Specifications
Because bool is fine until someone asks "but why did it fail?" Or even worse, your if condition is 10 lines of copy-pasta.
The Specification Pattern – a named DDD concept, not something I invented at 3am – lets you encapsulate business rules as composable, reusable objects that evaluate a context and tell you whether it passes, and why it doesn't.
Define a specification:
private class IsDraftSpecification : ISpecification<Invoice>
{
public SpecificationResult IsSatisfiedBy(Invoice invoice)
{
if (invoice.State != InvoiceState.Draft)
{
return new ErrorBuilder()
.WithCode("InvoiceMustBeDraft")
.WithMessage($"The invoice must be in the {InvoiceState.Draft} state.")
.Build();
}
return SpecificationResult.Satisfied;
}
}
SpecificationResult implicitly converts to bool and supports boolean operations, so simple checks stay simple:
var spec = new OrderTotalGreaterThan(100);
// Simple boolean path - no ceremony
if (!spec.IsSatisfiedBy(context))
return;
// Richer path
var result = spec.IsSatisfiedBy(context);
if (!result)
return result.Errors; // IReadOnlyList<Error> explaining exactly what went wrong
// Ultimate megatron evolution path
if (!spec.TrySatisfyBy(this, out var specResult))
{
throw new ErrorBuilder()
.WithCode("CannotChangeIssuer")
.WithMessage("The issuer of the invoice cannot be changed.")
.WithSpecificationResult(specResult)
.BuildValidationException();
}
Composition – one rule is never enough. Chain them fluently or use the classes directly:
// Fluent — reads like a sentence, composes like Lego
var spec = new OrderTotalGreaterThan(100)
.And(new CustomerIsVip())
.Or(new CustomerActiveYears(5));
// Or use the classes directly if you hate fluent APIs (I will judge tho)
var spec = new AndSpecification<OrderContext>(
new OrderTotalGreaterThan(100),
new CustomerIsVip());
All composition operators are available. &/| aggregate errors from both sides (greedy). &&/|| short-circuit when the outcome is deterministic (as it should be). Check out the docs at Boolean logical operators - AND, OR, NOT, XOR.
Negation — for when you want the opposite of a rule, with your own error message:
var notVip = new CustomerIsVip().Not(new ErrorBuilder()
.WithCode("CustomerIsVip")
.WithMessage("VIP customers are not eligible for this offer.")
.Build());
Tautology & Contradiction — for feature flags, defaults, and other situations where the rule is cosmically predetermined:
// Always true
var permissive = TautologySpecification<OrderContext>.Instance;
// Always false, with a custom error
var disabled = new ContradictionSpecification<OrderContext>(someError);
Exceptions, Errors, Results
Stuff happens. Sometimes it's your fault. Sometimes it's the network. Sometimes it's both.
This library supports both Result<T, Error> and exceptions for error handling in Domain-Driven Design (DDD). Pick the approach that best fits your project, your architecture, and your tolerance for boilerplate.
In general, prefer Result<T, Error> for business operations that can fail in expected ways. This keeps failures explicit, avoids control flow via exceptions, and makes error handling visible instead of somewhere up the call stack.
Exceptions still have their place. APIs, for example, often bubble errors straight to the top anyway, where they're translated into HTTP responses. In those cases, throwing an exception can be the simpler and more honest approach without passing Result<T, Error> throught twelve layers of services.
// Build errors fluently
var error = new ErrorBuilder()
.WithCode("InvalidInvoiceNumber")
.WithMessage("Invoice number must be after year 2000")
.WithDetail("Year", year.ToString())
// More details!: .WithDetail("Status", "caffeine overdose")
.Build();
// Convert to exceptions when you want to
throw error.ToBusinessRuleException();
Integrates well with FluentValidation, because writing the same validation logic twice is a crime:
// Error object:
if (new YourFluentValidator().Validate(someObject).TryGetError(out var error))
{
return error;
}
// Business exception:
new YourFluentValidator().ValidateAndThrowBusiness(result);
Result<T, E> and UnitResult<E> live in MartinDrozdik.DDD.Results. Both are readonly structs, so carrying an outcome around costs nothing on the heap:
public static Result<InvoiceNumber, Error> Create(int year, int order)
{
if (year < 2000)
{
return new ErrorBuilder()
.WithCode("InvalidInvoiceNumber")
.WithMessage("Invoice number must be after year 2000")
.Build(); // an Error implicitly converts to a failure
}
return new InvoiceNumber(year, order); // ...and a value converts to a success
}
// Compose without touching Value/Error by hand
var label = InvoiceNumber
.Create(year, order)
.Map(number => $"{number}")
.Match(text => text, error => error.Message);
Map, MapError, Bind, Ensure, Tap, TapError and Match all short-circuit on the first failure, and each has an Async counterpart.
Plenty of extension methods are provided for converting between errors, exceptions, and result types, so you can stay consistent without reinventing error plumbing every sprint.
Comes with WellKnownErrors for common cases. Feel free to ignore them and make your own (I won't tell).
Results come from CSharpFunctionalExtensions
Mediator - CQRS Without the Ceremony
Simple mediator pattern for Commands and Queries. No magic, just delegates work to handlers via DI.
Define a command:
public record CreateInvoiceCommand(/*params...*/) : ICommand<InvoiceId>;
Handle it:
public class CreateInvoiceCommandHandler : ICommandHandler<CreateInvoiceCommand, InvoiceId>
{
public async Task<InvoiceId> HandleAsync(
CreateInvoiceCommand command,
CancellationToken cancellationToken)
{
// Code
}
}
Register it:
builder.Services.AddMediator(config =>
{
config.WithCommand<CreateInvoiceCommand, InvoiceId, CreateInvoiceCommandHandler>(integration);
// ...
});
Send it:
var invoiceId = await mediator.SendCommand<CreateInvoiceCommand, InvoiceId>(
new CreateInvoiceCommand(/*params*/),
cancellationToken
);
Pipelines – we got them! And they are type-safe and nice! Pick who uses what pipelines! Add cross-cutting concerns like logging, validation, transactions, etc. without cluttering your handlers.
Using pipelines is super nice; but, to be honest, defining new pipelines requires a bit of boilerplate. Guys, it's a sacrifice for the type-safety, ok? In my defence, how often will you create new pipes huh?
Check out the demo app for examples of pipelines in action:
builder.Services.AddMediator(config =>
{
var integration = new LoggingPipelineIntegrator()
.Merge<ValidationPipelineIntegrator>();
config.WithCommand<CreateInvoiceCommand, InvoiceId, CreateInvoiceCommandHandler>(integration);
// ...
});
Message Outbox
A message that is enqueued atomically with the rest of the DDD updates. Such as a notification email, a webhook, or a message to another bounded context. It is a reliable, transactional queue that writes or reverts together with the domain updates.
Here is just contracts for the web transactional outbox (or other potential implementations).
An outbox message is a plain serializable record with a stable storage key:
public sealed record InvoiceDraftedMessage(Guid InvoiceId, string InvoiceNumber, string Recipient) : IOutboxMessage
{
public static OutboxMessageType MessageType => "invoice.drafted.v1";
}
A handler resolves it. Throw and it gets retried:
public class InvoiceDraftedMessageHandler(IEmailSender sender) : IOutboxMessageHandler<InvoiceDraftedMessage>
{
public Task HandleAsync(InvoiceDraftedMessage message, CancellationToken cancellationToken)
=> sender.SendAsync(message.Recipient, $"Invoice {message.InvoiceNumber} is ready.", cancellationToken);
}
IOutbox<TContext>.AddOnSave() enqueues it into the current transaction of TContext. Every context has its own outbox.
// DDD aggregate changes...
// Enqueue a message to be sent ONLY IF the changes commit
var message = new InvoiceDraftedMessage(invoice.Id.Key, invoiceNumber.ToString(), recipient.FullName);
outbox.AddOnSave(message);
// Save changes to the database
await context.SaveChangesAsync(cancellationToken); // the aggregate and the message, both or neither
Versioning should be done via the MessageType name. Append .v1, .v2, etc. to the name and register a new handler for the new version. The old messages will still be handled by the old handler. Outbox messages should not be long lived, so you can delete old handlers and messages after a while (or even immediately if you empty the outbox).
See the MartinDrozdik.DDD.Web README for the wiring, retries, dead-lettering and the rest of the engine.
Blob Storage
So you want to store files? We made that easy and transactional. Attachments, scans, exports - anything an aggregate refers to but is too big to keep in a column.
Here is just the contracts for the web blob storage (or other potential implementations).
A DDD object called Blob represents an immutable file. It has a stable storage key and a catalogue entry with metadata.
Blobs are categorized into separate containers and addresed by their key: {container}/{blobId}.
var key = BlobKey.Create(
"invoice-scans", // a kind of content (folder, bucket, container, etc.)
BlobId.New()); // this blob, a version 7 GUID
IBlobStore moves bytes and knows nothing about a database/catalogue. Safe for concurrent access with idempotent operations. It can move files, Ceph blobs, S3, Azure Blob Storage, etc.
IBlobStorage is a main entry catalogue that keeps track of Blob objects and their respective bytes stored via IBlobStore. It keeps the Blobs and their respective bytes in sync. No lingering files or rows with no content.
See the MartinDrozdik.DDD.Web README for the catalogue, the file store, transactional deletion and the sweep.
| 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
- FluentValidation (>= 12.2.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.14)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on MartinDrozdik.DDD:
| Package | Downloads |
|---|---|
|
MartinDrozdik.DDD.Options
DDD library extension allowing integration of .NET options with domain-driven design principles. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.11.0 | 152 | 10/31/2026 |
| 0.10.0 | 158 | 9/15/2026 |
| 0.9.1 | 145 | 8/31/2026 |
| 0.9.0 | 162 | 8/8/2026 |
| 0.8.1 | 155 | 8/7/2026 |
| 0.7.1 | 301 | 7/26/2026 |
| 0.7.0 | 351 | 5/16/2026 |
| 0.6.0 | 197 | 5/2/2026 |
| 0.5.3 | 209 | 4/19/2026 |
| 0.5.1 | 190 | 4/9/2026 |
| 0.5.0 | 194 | 4/6/2026 |
| 0.4.4 | 171 | 4/6/2026 |
| 0.4.3 | 135 | 4/6/2026 |
| 0.4.2 | 177 | 4/6/2026 |
| 0.4.1 | 178 | 4/6/2026 |
| 0.4.0 | 141 | 4/5/2026 |
| 0.3.3 | 208 | 3/21/2026 |
| 0.3.2 | 243 | 2/13/2026 |
| 0.3.1 | 169 | 2/9/2026 |
| 0.3.0 | 160 | 2/8/2026 |