Write 60% less code that reads like English using Railway-Oriented Programming and Domain-Driven Design
Transform error-prone imperative code into readable, succinct functional pipelinesβwith zero performance overhead.
// β Before: 20 lines of nested error checking
var firstName = ValidateFirstName(input.FirstName);
if (firstName == null) return BadRequest("Invalid first name");
var lastName = ValidateLastName(input.LastName);
if (lastName == null) return BadRequest("Invalid last name");
// ... 15 more lines of repetitive checks
// β
After: 8 lines that read like a story
return FirstName.TryCreate(input.FirstName)
.Combine(LastName.TryCreate(input.LastName))
.Combine(EmailAddress.TryCreate(input.Email))
.Bind((first, last, email) => User.TryCreate(first, last, email))
.Ensure(user => !_repository.EmailExists(user.Email), Error.Conflict("Email exists"))
.Tap(user => _repository.Save(user))
.Tap(user => _emailService.SendWelcome(user.Email))
.Match(onSuccess: user => Ok(user), onFailure: error => BadRequest(error.Detail));Key Benefits:
- π 60% less boilerplate - Write less, understand more
- π― Self-documenting - Code reads like English: "Create β Validate β Save β Notify"
- π Compiler-enforced - Impossible to skip error handling
- β‘ Zero overhead - Only 11-16ns (0.002% of I/O operations)
- β Production-ready - Type-safe, testable, maintainable
- Why Use This?
- Quick Start
- Key Features
- NuGet Packages
- Performance
- Documentation
- Examples
- What's New
- Contributing
- License
The Problem: Traditional error handling in C# creates verbose, error-prone code with nested if-statements that obscure business logic and make errors easy to miss.
The Solution: Railway-Oriented Programming (ROP) treats your code like railway tracksβoperations flow along the success track or automatically switch to the error track. You write what should happen, not what could go wrong.
Real-World Impact:
- β Teams report 40-60% reduction in error-handling boilerplate
- β Bugs caught at compile-time instead of runtime
- β New developers understand code faster thanks to readable chains
- β Zero performance penalty - same speed as imperative code
π Read the full introduction
Install the core railway-oriented programming package:
dotnet add package FunctionalDDD.RailwayOrientedProgrammingFor ASP.NET Core integration:
dotnet add package FunctionalDDD.Aspusing FunctionalDdd;
// Create a Result with validation
var emailResult = EmailAddress.TryCreate("user@example.com")
.Ensure(email => email.Domain != "spam.com",
Error.Validation("Email domain not allowed"))
.Tap(email => Console.WriteLine($"Valid email: {email}"));
// Handle success or failure
var message = emailResult.Match(
onSuccess: email => $"Welcome {email}!",
onFailure: error => $"Error: {error.Detail}"
);
// Chain multiple operations
var result = await GetUserAsync(userId)
.ToResultAsync(Error.NotFound("User not found"))
.BindAsync(user => SaveUserAsync(user))
.TapAsync(user => SendEmailAsync(user.Email));π Quick Start Guide
π Next Steps: Browse the Examples section or explore the complete documentation
π‘ Need help debugging? Check out the Debugging ROP Chains guide
Chain operations that automatically handle success/failure pathsβno more nested if-statements.
return GetUserAsync(id)
.ToResultAsync(Error.NotFound("User not found"))
.BindAsync(user => UpdateUserAsync(user))
.TapAsync(user => AuditLogAsync(user))
.MatchAsync(user => Ok(user), error => NotFound(error.Detail));Prevent primitive obsession and parameter mix-ups with strongly-typed domain objects.
// β
Compiler catches this mistake
CreateUser(lastName, firstName); // Error: Wrong parameter types!
// β This compiles but has a bug
CreateUser(lastNameString, firstNameString); // Swapped, but compiler can't tellPattern match on specific error types for precise error handling.
return ProcessOrder(order).MatchError(
onValidation: err => BadRequest(err.FieldErrors),
onNotFound: err => NotFound(err.Detail),
onConflict: err => Conflict(err.Detail),
onSuccess: order => Ok(order)
);Full support for async/await and parallel execution.
var result = await GetUserAsync(id)
.ParallelAsync(GetOrdersAsync(id))
.ParallelAsync(GetPreferencesAsync(id))
.AwaitAsync()
.MapAsync((user, orders, prefs) => new UserProfile(user, orders, prefs));OpenTelemetry integration for automatic distributed tracing.
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddFunctionalDddRopInstrumentation()
.AddOtlpExporter());π View all features
Functional programming, railway-oriented programming, and domain-driven design combine to create robust, reliable software.
Pure functions take inputs and produce outputs without side effects, making code predictable, testable, and composable.
π Applying Functional Principles in C# (Pluralsight)
Handle errors using a railway track metaphor: operations flow along the success track or automatically switch to the error track. This makes error handling explicit and visual.
Key insight: Write what should happen, not what could go wrong.
Focus on understanding the problem domain and creating an accurate model. Use Aggregates, Entities, and Value Objects to enforce business rules and maintain valid state.
π Domain-Driven Design in Practice (Pluralsight)
Pure Functions Clear business logic
+ β
Railway-Oriented β Explicit error handling
+ β
Type Safety β Compiler-enforced correctness
+ β
Domain Model β Business rule enforcement
= β
Robust, Maintainable Software
Recent enhancements:
- β¨ Discriminated Error Matching - Pattern match on specific error types (ValidationError, NotFoundError, etc.) using
MatchError - β¨ Tuple Destructuring - Automatically destructure tuples in Match/Switch for cleaner code
- π Enhanced Documentation - Complete documentation site with tutorials, examples, and API reference
- β‘ Performance Optimizations - Reduced allocation and improved throughput
- π OpenTelemetry Tracing - Built-in distributed tracing support
π View changelog
| Package | Version | Description | Documentation |
|---|---|---|---|
| RailwayOrientedProgramming | Core Result/Maybe types, error handling, async support | π Docs | |
| Asp | Convert Result β HTTP responses (MVC & Minimal API) | π Docs | |
| Http | HTTP client extensions for Result/Maybe with status code handling | π Docs | |
| FluentValidation | Integrate FluentValidation with ROP | π Docs | |
| CommonValueObjects | RequiredString, RequiredGuid, EmailAddress | π Docs | |
| CommonValueObjectGenerator | Source generator for value object boilerplate | π Docs | |
| DomainDrivenDesign | Aggregate, Entity, ValueObject, Domain Events | π Docs | |
| Testing | FluentAssertions extensions, test builders, fakes | π Docs |
Comprehensive benchmarks on .NET 10 show ROP adds only 11-16 nanoseconds of overheadβless than 0.002% of typical I/O operations.
| Operation | Time | Overhead | Memory |
|---|---|---|---|
| Happy Path | 147 ns | 16 ns (12%) | 144 B |
| Error Path | 99 ns | 11 ns (13%) | 184 B |
| Combine (5 results) | 58 ns | - | 0 B |
| Bind chain (5) | 63 ns | - | 0 B |
Real-world context:
Database Query: 1,000,000 ns (1 ms)
ROP Overhead: 16 ns
β
0.0016% of DB query time
The overhead is 1/62,500th of a single database query!
β
Same memory usage as imperative code
β‘ Single-digit to low double-digit nanosecond operations
π View detailed benchmarks
Run benchmarks yourself:
dotnet run --project Benchmark/Benchmark.csproj -c Releaseπ Complete Documentation Site
π Beginner (2-3 hours)
- Introduction - Why use ROP?
- Basics Tutorial - Core concepts
- Examples - Real-world patterns
πΌ Integration (1-2 hours)
π Advanced (3-4 hours)
- Clean Architecture - CQRS patterns
- Advanced Features - LINQ, parallelization
- Error Handling - Custom errors, aggregation
// Chain operations with automatic error handling
var result = EmailAddress.TryCreate("user@example.com")
.Ensure(email => email.Domain != "spam.com", Error.Validation("Domain not allowed"))
.Tap(email => _logger.LogInformation("Validated: {Email}", email))
.Match(
onSuccess: email => $"Welcome {email}!",
onFailure: error => $"Error: {error.Detail}"
);User Registration with Validation
[HttpPost]
public ActionResult<User> Register([FromBody] RegisterUserRequest request) =>
FirstName.TryCreate(request.FirstName)
.Combine(LastName.TryCreate(request.LastName))
.Combine(EmailAddress.TryCreate(request.Email))
.Bind((first, last, email) => User.TryCreate(first, last, email, request.Password))
.Ensure(user => !_repository.EmailExists(user.Email), Error.Conflict("Email exists"))
.Tap(user => _repository.Save(user))
.Tap(user => _emailService.SendWelcome(user.Email))
.ToActionResult(this);Async Operations
public async Task<IResult> ProcessOrderAsync(int orderId)
{
return await GetOrderAsync(orderId)
.ToResultAsync(Error.NotFound($"Order {orderId} not found"))
.EnsureAsync(
order => order.CanProcessAsync(),
Error.Validation("Order cannot be processed"))
.TapAsync(order => ValidateInventoryAsync(order))
.BindAsync(order => ChargePaymentAsync(order))
.TapAsync(order => SendConfirmationAsync(order))
.MatchAsync(
order => Results.Ok(order),
error => Results.BadRequest(error.Detail));
}Parallel Operations
// Fetch data from multiple sources in parallel
var result = await GetUserAsync(userId)
.ParallelAsync(GetOrdersAsync(userId))
.ParallelAsync(GetPreferencesAsync(userId))
.AwaitAsync()
.BindAsync(
(user, orders, preferences) =>
CreateProfileAsync(user, orders, preferences),
ct);Discriminated Error Matching
return ProcessOrder(order).MatchError(
onValidation: err => Results.BadRequest(new { errors = err.FieldErrors }),
onNotFound: err => Results.NotFound(new { message = err.Detail }),
onConflict: err => Results.Conflict(new { message = err.Detail }),
onUnauthorized: _ => Results.Unauthorized(),
onSuccess: order => Results.Ok(order)
);HTTP Integration
// Read HTTP response as Result with status code handling
var result = await _httpClient.GetAsync($"api/users/{userId}", ct)
.HandleNotFoundAsync(Error.NotFound("User not found"))
.HandleUnauthorizedAsync(Error.Unauthorized("Please login"))
.HandleServerErrorAsync(code => Error.ServiceUnavailable($"API error: {code}"))
.ReadResultFromJsonAsync(UserContext.Default.User, ct)
.TapAsync(user => _logger.LogInformation("Retrieved user: {UserId}", user.Id));
// Or use EnsureSuccess for generic error handling
var product = await _httpClient.GetAsync($"api/products/{productId}", ct)
.EnsureSuccessAsync(code => Error.Unexpected($"Failed to get product: {code}"))
.ReadResultFromJsonAsync(ProductContext.Default.Product, ct);FluentValidation Integration
public class User : Aggregate<UserId>
{
public FirstName FirstName { get; }
public LastName LastName { get; }
public EmailAddress Email { get; }
public static Result<User> TryCreate(FirstName firstName, LastName lastName, EmailAddress email)
{
var user = new User(firstName, lastName, email);
return Validator.ValidateToResult(user);
}
private static readonly InlineValidator<User> Validator = new()
{
v => v.RuleFor(x => x.FirstName).NotNull(),
v => v.RuleFor(x => x.LastName).NotNull(),
v => v.RuleFor(x => x.Email).NotNull(),
};
}π Browse all examples | π Complete documentation
Contributions are welcome! This project follows standard GitHub workflow:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please ensure:
- β
All tests pass (
dotnet test) - β Code follows existing style conventions
- β New features include tests and documentation
- β Commit messages are clear and descriptive
For major changes, please open an issue first to discuss what you would like to change.
π Contributing Guide
This project is licensed under the MIT License - see the LICENSE file for details.
- CSharpFunctionalExtensions - Functional Extensions for C# by Vladimir Khorikov. This library was inspired by Vladimir's excellent training materials and takes a complementary approach with enhanced DDD support and comprehensive documentation.
- π Documentation
- π¬ Discussions - Ask questions, share ideas
- π Issues - Report bugs or request features
- β Star this repo if you find it useful!
- π₯ YouTube: Functional DDD Explanation - Third-party video explaining the library concepts
- π Pluralsight: Applying Functional Principles in C#
- π Pluralsight: Domain-Driven Design in Practice
Made with β€οΈ by the FunctionalDDD community