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
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));?? 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
.AddRailwayOrientedProgrammingInstrumentation()
.AddOtlpExporter());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:
- ?? NEW: EnumValueObject - Type-safe enumerations with behavior, state machine support, and JSON serialization. Prevents invalid values unlike C# enums.
- ?? NEW: Roslyn Analyzers - 14 compile-time diagnostics to enforce ROP best practices and prevent common mistakes with Result/Maybe types
- ? ASP.NET Core Auto-Validation - Value objects automatically validate in requests (route params, query strings, JSON bodies) via
AddScalarValueObjectValidation() - ?? 11 New Value Objects - Ready-to-use:
Url,PhoneNumber,Percentage,Currency,IpAddress,Hostname,Slug,CountryCode,LanguageCode,Age, plusRequiredInt/RequiredDecimal - ? 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
| 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 | |
| PrimitiveValueObjects | Base classes (RequiredString, RequiredGuid, RequiredUlid, RequiredInt, RequiredDecimal) + 11 ready-to-use VOs | ?? Docs | |
| PrimitiveValueObjectGenerator | Source generator for value object boilerplate | ?? Docs | |
| Analyzers | NEW! Roslyn analyzers for compile-time ROP safety (14 rules) | ?? Docs | |
| DomainDrivenDesign | Aggregate, Entity, ValueObject, EnumValueObject, 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.
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