API Documentation for FunctionalDdd Library
Welcome to the FunctionalDDD API reference. This library brings Railway-Oriented Programming (ROP) and Domain-Driven Design (DDD) patterns to C#, enabling you to write robust, maintainable code with explicit error handling.
Core Packages
Railway-Oriented Programming
New to Railway-Oriented Programming? The concept uses a railway track analogy where operations flow along a success track or switch to an error track. For a gentle introduction to the philosophy, see this introductory article. Our documentation provides a comprehensive, production-ready C# implementation with type safety, async support, and real-world patterns.
Result<T>
The Result<T> monad represents an operation that can either succeed with a value of type T or fail with an Error. This is the foundation of Railway-Oriented Programming, enabling explicit error handling without exceptions.
Type Definition:
public readonly struct Result<T>
{
public bool IsSuccess { get; }
public bool IsFailure { get; }
public T Value { get; } // Available when IsSuccess is true
public Error Error { get; } // Available when IsFailure is true
}
Core Operations:
Bind- Chain operations that return Result (railway switching)Map- Transform success values while preserving errorsTap- Execute side effects without changing the result (logging, events, notifications)Ensure- Apply business rule validationMatch- Pattern match on success/failure for final handlingCombine- Aggregate multiple results and collect all errors (parallel validation)Compensate- Provide fallback values on failureMapError- Transform error typesWhen- Conditional execution based on predicates
Common Usage Patterns:
Railway Pattern - Sequential Chaining:
var result = UserId.TryCreate(id)
.Bind(repository.GetUser)
.Map(user => new UserDto(user))
.Tap(dto => logger.LogInformation("User retrieved: {Id}", dto.Id))
.Match(
onSuccess: dto => Ok(dto),
onFailure: error => error.ToActionResult(this)
);
Combine - Parallel Validation (Collects ALL Errors):
var result = EmailAddress.TryCreate(email)
.Combine(FirstName.TryCreate(firstName))
.Combine(LastName.TryCreate(lastName))
.Bind((email, first, last) => User.Create(email, first, last));
Tap - Side Effects Without Breaking the Chain:
var result = order.Submit()
.Tap(o => logger.LogInformation("Order {Id} submitted", o.Id))
.Tap(o => eventBus.Publish(new OrderSubmittedEvent(o.Id)))
.Tap(o => emailService.SendConfirmation(o.CustomerEmail))
.Tap(o => analytics.Track("OrderSubmitted", o.Id));
// Result is still Result<Order> after all Taps
Combine vs Bind:
// Combine: ALL validations run, ALL errors collected
var result = ProductName.TryCreate(name) // ❌ Error: "Name too short"
.Combine(Price.TryCreate(price)) // ❌ Error: "Price must be positive"
.Combine(Quantity.TryCreate(quantity)); // ✅ Success
// Returns errors for BOTH name AND price
// Bind: Sequential - stops at first error
var result = ProductName.TryCreate(name) // ❌ Error: "Name too short"
.Bind(n => Price.TryCreate(price) // ⏹️ Never executed
.Bind(p => Quantity.TryCreate(quantity)));
// Only returns the name error
Factory Methods:
Result.Success<T>(T value)- Create a successful resultResult.Failure<T>(Error error)- Create a failed result
Using Result<Unit> for Void Operations
When an operation doesn't return a value (like Delete or Update commands), use Result<Unit>:
What is Unit?
Unitis a special type representing "no value"- Similar to
voidbut can be used as a generic type parameter Result<Unit>indicates success/failure without a data payload
Type:
Result<Unit> // Success or failure, no value
Common Use Cases:
- Delete operations
- Update operations that don't return data
- Void commands
- Operations that modify state
Example:
// Void operation that can fail
public Result<Unit> DeleteUser(UserId id) =>
repository.Delete(id)
.Tap(() => eventBus.Publish(new UserDeletedEvent(id)));
// In API controllers, Unit results automatically return 204 No Content
[HttpDelete("{id}")]
public ActionResult<Unit> DeleteUser(Guid id) =>
UserId.TryCreate(id)
.Bind(DeleteUserCommand)
.ToActionResult(this); // Returns 204 No Content on success
Factory Methods for Unit Results:
Result.Success()- Create successful unit resultResult.Failure(Error error)- Create failed unit result
Maybe<T>
The Maybe<T> monad represents an optional value that may or may not exist. Use it to eliminate null reference exceptions and make optionality explicit in your type system. Think of it as a type-safe alternative to nullable references.
Type Definition:
public readonly struct Maybe<T>
{
public bool HasValue { get; }
public bool HasNoValue { get; }
public T Value { get; } // Throws if HasNoValue is true
}
Core Operations:
GetValueOrDefault- Safely extract value with fallbackTryGetValue- Try to get the value (out parameter pattern)GetValueOrThrow- Get value or throw exceptionToResult- Convert to Result (None becomes an Error)AsMaybe- Convert from nullable to MaybeAsNullable- Convert Maybe back to nullable
Properties:
HasValue- Check if value is presentHasNoValue- Check if value is absentValue- Get value (throws if absent)
Common Patterns:
// Repository query (null-safe)
Maybe<User> FindUserByEmail(EmailAddress email);
// Safe value extraction with fallback
var user = repository.FindUserByEmail(email);
var userName = user.HasValue
? user.Value.FullName
: "Unknown User";
// Or using GetValueOrDefault
var userName = repository.FindUserByEmail(email)
.GetValueOrDefault(defaultUser)
.FullName;
// Convert to Result for further processing
var result = repository.FindUserByEmail(email)
.ToResult(Error.NotFound("User", email.Value))
.Bind(user => user.UpdateProfile(newData));
// Try pattern for safe access
if (repository.FindUserByEmail(email).TryGetValue(out var user))
{
// Use user safely
ProcessUser(user);
}
Factory Methods:
Maybe.From<T>(T value)- Create Maybe from nullable value (null becomes None)Maybe.None<T>()- Create an empty Maybe
When to Use:
- Maybe: When absence is a valid, expected state (e.g., optional config, search results)
- Result: When you need to communicate why something failed (validation, business rules)
Error Types
A hierarchy of error types for representing different failure scenarios:
ValidationError- Input validation failures with field-level detailsNotFoundError- Resource not found (404)UnauthorizedError- Authentication required (401)ForbiddenError- Insufficient permissions (403)ConflictError- Resource conflicts (409)DomainError- Business rule violations (422)UnexpectedError- Unexpected system errors (500)ServiceUnavailableError- Service unavailable (503)
Domain-Driven Design
Aggregate
A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An aggregate will have one of its component objects be the aggregate root. Any references from outside the aggregate should only go to the aggregate root. The root can thus ensure the integrity of the aggregate as a whole.
Key features: Encapsulation, consistency boundaries, transactional boundaries
Read more about DDD Aggregates
Entity
A domain object that has a unique identity that runs through time and different representations. Two entities with the same identity are considered the same, even if their attributes differ.
ValueObject
A value object is an object that represents a descriptive aspect of the domain with no conceptual identity. It is a small, simple object that encapsulates a concept from your problem domain. Unlike an aggregate, a value object does not have a unique identity and is immutable. Value objects support and enrich the ubiquitous language of your domain.
Key characteristics: Immutability, equality by value, no identity
Integration Packages
ASP.NET Core Integration
ActionResult Extensions
Convert Result<T> to ASP.NET Core action results for MVC controllers.
Features: Automatic status code mapping, Problem Details (RFC 7807) format, field-level validation errors
HttpResult Extensions
Convert Result<T> to IResult for Minimal API endpoints.
Features: Fluent error matching, pagination support, Unit type handling (204 No Content)
HTTP Client Integration
HttpResponseExtensions
Extension methods for HttpResponseMessage that enable functional HTTP communication with Result types.
Key methods:
HandleNotFound/HandleUnauthorized/HandleForbidden/HandleConflict- Handle specific status codesHandleClientError/HandleServerError- Handle error ranges (4xx, 5xx)EnsureSuccess- Functional alternative toEnsureSuccessStatusCode()ReadResultFromJsonAsync- Deserialize JSON toResult<T>ReadResultMaybeFromJsonAsync- Deserialize JSON toResult<Maybe<T>>
FluentValidation Integration
FluentValidation Extensions
Convert FluentValidation results to Result<T> for seamless integration with Railway-Oriented Programming.
Features: Automatic ValidationError creation, field-level error mapping, async validation support
Common Value Objects
EmailAddress
A validated email address value object. Ensures email format correctness at construction.
RequiredString
A non-null, non-empty string value object. Prevents primitive obsession for required text fields.
RequiredGuid
A validated GUID value object that cannot be empty (Guid.Empty).
ScalarValueObject
Base class for creating custom value objects that wrap a single primitive value.
Common examples: FirstName, LastName, OrderId, UserId, ProductCode
Observability
OpenTelemetry Tracing
Built-in distributed tracing support for Railway-Oriented Programming operations.
Features:
- Automatic span creation for
Bind,Map,Tap,Ensureoperations - Error tracking and status codes
- Trace correlation across async operations
- Integration with Jaeger, Zipkin, Application Insights
Next Steps
- Get Started: Read the Introduction guide
- Learn Basics: Explore Core Concepts
- Integration: See ASP.NET Core, HTTP Client, and FluentValidation guides
- Examples: Browse Real-World Examples