MartinDrozdik.DDD
0.7.0
dotnet add package MartinDrozdik.DDD --version 0.7.0
NuGet\Install-Package MartinDrozdik.DDD -Version 0.7.0
<PackageReference Include="MartinDrozdik.DDD" Version="0.7.0" />
<PackageVersion Include="MartinDrozdik.DDD" Version="0.7.0" />
<PackageReference Include="MartinDrozdik.DDD" />
paket add MartinDrozdik.DDD --version 0.7.0
#r "nuget: MartinDrozdik.DDD, 0.7.0"
#:package MartinDrozdik.DDD@0.7.0
#addin nuget:?package=MartinDrozdik.DDD&version=0.7.0
#tool nuget:?package=MartinDrozdik.DDD&version=0.7.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...
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 hadling 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<Guid> // or IAggregateRoot<Guid>
{
private Person(Guid id, string fullName)
{
Id = id;
FullName = fullName;
}
public Guid 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. Compares by value. Extends like a class. It's beautiful 😍.
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 and Errors
Stuff happens. Sometimes it's your fault. Sometimes it's the network. Sometimes it's both.
This library supports both Result<T> 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> 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> 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);
Plenty of extension methods are provided for converting between errors, exceptions, and Result<T> 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).
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);
// ...
});
| 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
- CSharpFunctionalExtensions (>= 3.7.0)
- FluentValidation (>= 12.1.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.8)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on MartinDrozdik.DDD:
| Package | Downloads |
|---|---|
|
MartinDrozdik.DDD.Web
A library to support web domain-driven design development with solutions to common problems. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.7.0 | 281 | 5/16/2026 |
| 0.6.0 | 161 | 5/2/2026 |
| 0.5.3 | 174 | 4/19/2026 |
| 0.5.1 | 161 | 4/9/2026 |
| 0.5.0 | 164 | 4/6/2026 |
| 0.4.4 | 137 | 4/6/2026 |
| 0.4.3 | 93 | 4/6/2026 |
| 0.4.2 | 141 | 4/6/2026 |
| 0.4.1 | 146 | 4/6/2026 |
| 0.4.0 | 110 | 4/5/2026 |
| 0.3.3 | 179 | 3/21/2026 |
| 0.3.2 | 216 | 2/13/2026 |
| 0.3.1 | 138 | 2/9/2026 |
| 0.3.0 | 122 | 2/8/2026 |
| 0.2.0 | 123 | 1/18/2026 |
| 0.1.0 | 117 | 1/17/2026 |