DDD building blocks and Result pattern. Zero dependencies.
$ dotnet add package Fluens.KernelDDD building blocks and Result pattern. Zero dependencies.
dotnet add package Fluens.Kernel
// Result without value
Result result = Result.Success();
Result failure = Result.Failure(new NotFoundError("User not found"));
// Result with value
Result<int> result = Result.Success(42);
Result<int> failure = Result.Failure<int>(new ValidationError("Invalid input"));
// Pattern matching
string message = result.Match(
onSuccess: value => $"Got: {value}",
onFailure: error => $"Error: {error.Message}"
);
Both Result and Result<T> are readonly struct types — zero heap allocations, default(Result) is a valid success.
// Implicit: value → Result<T>
Result<int> result = 42;
// Explicit: Result<T> → Result (discards value)
Result plain = (Result)result;
// As<T>(): failure Result → Result<T>
Result failure = Result.Failure(new NotFoundError("Missing"));
Result<int> typed = failure.As<int>(); // preserves error
// As<T>() on success throws InvalidOperationException
Built-in error types: NotFoundError, ValidationError, ConflictError, ForbiddenError.
Errors support parameterized message arguments for translations via MessageArgs and fluent WithArg:
// Create error with message arguments for parameterized translations
var error = new NotFoundError("Order {orderId} not found")
.WithArg("orderId", orderId.ToString());
// Access arguments
error.MessageArgs!["orderId"]; // "123"
// Chain multiple arguments
var error = new Error("CUSTOM", "Hello {name}, you have {count} items")
.WithArg("name", "John")
.WithArg("count", "5");
WithArg is immutable — each call returns a new Error instance without mutating the original. MessageArgs is IReadOnlyDictionary<string, string>? (null by default).
Standard error codes are centralized in the ErrorCodes static class:
ErrorCodes.NotFound // "NOT_FOUND"
ErrorCodes.Validation // "VALIDATION"
ErrorCodes.Conflict // "CONFLICT"
ErrorCodes.Forbidden // "FORBIDDEN"
Use these constants instead of hardcoded strings when comparing error codes:
if (result.Error.Code == ErrorCodes.NotFound)
{
// handle not found
}
// ITypedIdentifier<out T> is covariant — enables polymorphic identifier handling
public readonly record struct OrderId(int Value) : ITypedIdentifier<int>;
// Entities with identity-based equality
public class Order : AggregateRoot<OrderId>
{
public string Number { get; private set; } = "";
public void Place()
{
AddEvent(new OrderPlacedEvent(ID));
}
}
// Value objects with structural equality
public sealed record Money(decimal Amount, string Currency) : ValueObject
{
protected override IEnumerable<object?> GetAtomicValues()
{
yield return Amount;
yield return Currency;
}
}
Interfaces: IAuditable (CreatedAtUtc, UpdatedAtUtc), IDeletable (DeletedAtUtc), IDomainEvent (marker interface — no members).
Filter out soft-deleted entities from IQueryable<T> sources:
IQueryable<Order> activeOrders = dbContext.Orders.WithoutDeleted();
WithoutDeleted() returns only entities where DeletedAtUtc is null.
TranslatableMessage is a structure for storing translatable content (e.g. audit log entries) as JSON in a database. The message can be translated at read time using a ResourceManager:
// Create a translatable message with arguments
var message = new TranslatableMessage("order.status.changed")
.WithArg("orderId", "ORD-123")
.WithArg("oldStatus", "Pending")
.WithArg("newStatus", "Shipped");
// Serialize to JSON for database storage (compact property names)
var json = JsonSerializer.Serialize(message, TranslatableMessageSerializerContext.Default.TranslatableMessage);
// {"c":"order.status.changed","a":{"orderId":"ORD-123","oldStatus":"Pending","newStatus":"Shipped"}}
// Translate at read time using ResourceManager
var translated = message.Translate(resourceManager, CultureInfo.CurrentUICulture);
// "Order ORD-123 changed from Pending to Shipped"
WithArg is immutable — each call returns a new TranslatableMessage instance. TranslatableMessageSerializerContext provides AOT-compatible JSON serialization with WhenWritingNull to omit null Args.
This project is licensed under the MIT License.