FunctionalDdd.DomainDrivenDesign
3.0.0-alpha.13
See the version list below for details.
dotnet add package FunctionalDdd.DomainDrivenDesign --version 3.0.0-alpha.13
NuGet\Install-Package FunctionalDdd.DomainDrivenDesign -Version 3.0.0-alpha.13
<PackageReference Include="FunctionalDdd.DomainDrivenDesign" Version="3.0.0-alpha.13" />
<PackageVersion Include="FunctionalDdd.DomainDrivenDesign" Version="3.0.0-alpha.13" />
<PackageReference Include="FunctionalDdd.DomainDrivenDesign" />
paket add FunctionalDdd.DomainDrivenDesign --version 3.0.0-alpha.13
#r "nuget: FunctionalDdd.DomainDrivenDesign, 3.0.0-alpha.13"
#:package FunctionalDdd.DomainDrivenDesign@3.0.0-alpha.13
#addin nuget:?package=FunctionalDdd.DomainDrivenDesign&version=3.0.0-alpha.13&prerelease
#tool nuget:?package=FunctionalDdd.DomainDrivenDesign&version=3.0.0-alpha.13&prerelease
Domain Driven Design
Building blocks for implementing Domain-Driven Design tactical patterns in C# with functional programming principles.
Installation
dotnet add package FunctionalDDD.DomainDrivenDesign
Quick Start
Entity
Objects with unique identity. Equality based on ID.
public class CustomerId : ScalarValueObject<Guid>
{
private CustomerId(Guid value) : base(value) { }
public static CustomerId NewUnique() => new(Guid.NewGuid());
}
public class Customer : Entity<CustomerId>
{
public string Name { get; private set; }
private Customer(CustomerId id, string name) : base(id)
{
Name = name;
}
public static Result<Customer> TryCreate(string name) =>
name.ToResult()
.Ensure(n => !string.IsNullOrWhiteSpace(n), Error.Validation("Name required"))
.Map(n => new Customer(CustomerId.NewUnique(), n));
}
Value Object
Immutable objects with no identity. Equality based on all properties.
public class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
private Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
public static Result<Money> TryCreate(decimal amount, string currency = "USD") =>
(amount, currency).ToResult()
.Ensure(x => x.amount >= 0, Error.Validation("Amount cannot be negative"))
.Map(x => new Money(x.amount, x.currency));
protected override IEnumerable<IComparable> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
public Money Add(Money other) =>
Currency == other.Currency
? new Money(Amount + other.Amount, Currency)
: throw new InvalidOperationException("Currency mismatch");
}
Aggregate
Cluster of entities and value objects treated as a unit. Manages domain events.
public record OrderCreatedEvent(OrderId Id, CustomerId CustomerId) : IDomainEvent;
public record OrderSubmittedEvent(OrderId Id, Money Total) : IDomainEvent;
public class Order : Aggregate<OrderId>
{
private readonly List<OrderLine> _lines = [];
public CustomerId CustomerId { get; }
public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
public Money Total { get; private set; }
public OrderStatus Status { get; private set; }
private Order(OrderId id, CustomerId customerId) : base(id)
{
CustomerId = customerId;
Status = OrderStatus.Draft;
Total = Money.TryCreate(0).Value;
DomainEvents.Add(new OrderCreatedEvent(id, customerId));
}
public static Result<Order> TryCreate(CustomerId customerId) =>
new Order(OrderId.NewUnique(), customerId).ToResult();
public Result<Order> AddLine(ProductId productId, string name, Money price, int qty) =>
this.ToResult()
.Ensure(_ => Status == OrderStatus.Draft, Error.Validation("Order not editable"))
.Ensure(_ => qty > 0, Error.Validation("Quantity must be positive"))
.Tap(_ =>
{
_lines.Add(new OrderLine(productId, name, price, qty));
RecalculateTotal();
});
public Result<Order> Submit() =>
this.ToResult()
.Ensure(_ => Status == OrderStatus.Draft, Error.Validation("Already submitted"))
.Ensure(_ => Lines.Count > 0, Error.Validation("Cannot submit empty order"))
.Tap(_ =>
{
Status = OrderStatus.Submitted;
DomainEvents.Add(new OrderSubmittedEvent(Id, Total));
});
private void RecalculateTotal()
{
var total = Lines.Sum(l => l.Price.Amount * l.Quantity);
Total = Money.TryCreate(total).Value;
}
}
Domain Events
Publish events after persisting:
var order = Order.TryCreate(customerId)
.Bind(o => o.AddLine(productId, "Widget", price, 5))
.Bind(o => o.Submit());
if (order.IsSuccess)
{
await repository.SaveAsync(order.Value);
foreach (var evt in order.Value.UncommittedEvents())
{
await eventBus.PublishAsync(evt);
}
order.Value.AcceptChanges();
}
Best Practices
1. Use entities when identity matters
public class Customer : Entity<CustomerId> { } // Identity-based
public class Address : ValueObject { } // Value-based
2. Keep aggregates small
public class Order : Aggregate<OrderId>
{
// ? Include: OrderLine (part of aggregate)
// ? Exclude: Customer, Shipment (reference by ID)
public CustomerId CustomerId { get; }
}
3. Reference other aggregates by ID
// ? Good
public CustomerId CustomerId { get; }
// ? Avoid
public Customer Customer { get; }
4. Enforce invariants in aggregate root
public Result<Order> AddLine(...) =>
this.ToResult()
.Ensure(_ => Status == OrderStatus.Draft, ...)
.Ensure(_ => quantity > 0, ...)
.Tap(_ => _lines.Add(...));
5. Use domain events for side effects
// ? Good - domain event
DomainEvents.Add(new OrderSubmittedEvent(Id));
// ? Avoid - direct coupling
_emailService.SendConfirmation();
6. Validate using Result types
// ? Good
public Result<Order> Cancel(string reason) =>
this.ToResult()
.Ensure(_ => Status == OrderStatus.Draft, ...);
// ? Avoid
if (Status != OrderStatus.Draft)
throw new InvalidOperationException(...);
7. Make value objects immutable
// ? Good
public decimal Amount { get; } // No setter
// ? Avoid
public decimal Amount { get; set; }
Core Concepts
Entity<TId>
- Identity-based equality
- Mutable state
- Lifecycle tracked by ID
ValueObject
- No identity
- Immutable
- Equality based on all properties
- Override
GetEqualityComponents()
ScalarValueObject<T>
- Wraps single value
- Type safety for primitives
- Implicit conversion to
T
Aggregate<TId>
- Consistency boundary
- Manages domain events
- Properties:
IsChanged,UncommittedEvents(),AcceptChanges()
IDomainEvent
- Marker interface for domain events
- Use records for immutability
- Publish after persistence
Resources
- SAMPLES.md - Comprehensive examples and patterns
- Main Documentation - Full repository documentation
- Railway Oriented Programming - Result type and functional patterns
License
MIT License - see LICENSE file for details
| 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
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on FunctionalDdd.DomainDrivenDesign:
| Package | Downloads |
|---|---|
|
FunctionalDdd.CommonValueObjects
To avoid passing around strings, it is recommended to use RequiredString to obtain strongly typed properties. The source code generator will automate the implementation process. |
|
|
FunctionalDDD.Testing
Testing utilities and assertions for FunctionalDDD - FluentAssertions extensions, test builders, and fake implementations for Railway-Oriented Programming |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.0-alpha.20 | 37 | 1/6/2026 |
| 3.0.0-alpha.19 | 40 | 1/5/2026 |
| 3.0.0-alpha.13 | 41 | 1/5/2026 |
| 3.0.0-alpha.9 | 42 | 1/5/2026 |
| 3.0.0-alpha.3 | 146 | 12/20/2025 |
| 2.1.10 | 693 | 12/3/2025 |
| 2.1.9 | 280 | 11/21/2025 |
| 2.1.1 | 247 | 4/26/2025 |
| 2.1.0-preview.3 | 98 | 4/26/2025 |
| 2.0.1 | 245 | 1/23/2025 |
| 2.0.0-alpha.62 | 87 | 1/8/2025 |
| 2.0.0-alpha.61 | 91 | 1/7/2025 |
| 2.0.0-alpha.60 | 104 | 12/7/2024 |
| 2.0.0-alpha.55 | 93 | 11/22/2024 |
| 2.0.0-alpha.52 | 96 | 11/7/2024 |
| 2.0.0-alpha.48 | 96 | 11/2/2024 |
| 2.0.0-alpha.47 | 94 | 10/30/2024 |
| 2.0.0-alpha.44 | 157 | 10/18/2024 |
| 2.0.0-alpha.42 | 106 | 10/14/2024 |
| 2.0.0-alpha.39 | 135 | 6/27/2024 |
| 2.0.0-alpha.38 | 115 | 4/24/2024 |
| 2.0.0-alpha.33 | 99 | 4/17/2024 |
| 2.0.0-alpha.26 | 137 | 4/9/2024 |
| 2.0.0-alpha.21 | 119 | 4/1/2024 |
| 2.0.0-alpha.19 | 108 | 3/5/2024 |
| 2.0.0-alpha.18 | 103 | 2/28/2024 |
| 2.0.0-alpha.17 | 104 | 2/26/2024 |
| 2.0.0-alpha.15 | 107 | 1/30/2024 |
| 2.0.0-alpha.8 | 99 | 1/27/2024 |
| 2.0.0-alpha.6 | 134 | 1/5/2024 |
| 1.1.1 | 812 | 11/15/2023 |
| 1.1.0-alpha.32 | 165 | 11/2/2023 |
| 1.1.0-alpha.30 | 244 | 10/31/2023 |
| 1.1.0-alpha.28 | 126 | 10/28/2023 |
| 1.1.0-alpha.27 | 127 | 10/28/2023 |
| 1.1.0-alpha.24 | 125 | 10/20/2023 |
| 1.1.0-alpha.23 | 128 | 10/13/2023 |
| 1.1.0-alpha.21 | 165 | 10/1/2023 |
| 1.1.0-alpha.20 | 122 | 9/30/2023 |
| 1.1.0-alpha.19 | 152 | 9/30/2023 |
| 1.1.0-alpha.18 | 145 | 9/29/2023 |
| 1.1.0-alpha.17 | 120 | 9/22/2023 |
| 1.1.0-alpha.13 | 114 | 9/16/2023 |
| 1.1.0-alpha.4 | 236 | 6/9/2023 |
| 1.1.0-alpha.3 | 167 | 6/8/2023 |
| 1.0.1 | 864 | 5/12/2023 |
| 0.1.0-alpha.40 | 213 | 4/6/2023 |
| 0.1.0-alpha.39 | 227 | 4/3/2023 |
| 0.1.0-alpha.38 | 251 | 4/2/2023 |
| 0.1.0-alpha.37 | 229 | 3/31/2023 |
| 0.1.0-alpha.35 | 218 | 3/29/2023 |
| 0.1.0-alpha.34 | 190 | 3/28/2023 |
| 0.1.0-alpha.32 | 234 | 3/18/2023 |
| 0.1.0-alpha.30 | 227 | 3/11/2023 |
| 0.1.0-alpha.27 | 231 | 3/7/2023 |
| 0.1.0-alpha.24 | 226 | 2/15/2023 |
| 0.1.0-alpha.22 | 222 | 2/15/2023 |
| 0.1.0-alpha.20 | 219 | 2/13/2023 |
| 0.1.0-alpha.19 | 188 | 2/13/2023 |
| 0.0.1-alpha.14 | 248 | 1/4/2023 |
| 0.0.1-alpha.4 | 216 | 12/30/2022 |